@gpmpay/sdk
Version:
Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.
436 lines (418 loc) • 18.7 kB
text/typescript
import { R as RequestOptions, P as Page, L as ListParams, a as ListWebhookHistoriesParams, W as WebhookHistory, b as WebhookSetting, c as ListWebhookSettingsParams, C as CreateWebhookSettingParams, U as UpdateWebhookSettingParams } from './verify-DVgS9Vh1.cjs';
export { A as AuthFailureReason, d as ConfigErrorCode, E as EVENT_HEADER, G as GpmPayAPIError, e as GpmPayAuthenticationError, f as GpmPayBadRequestError, g as GpmPayConfigError, h as GpmPayConnectionError, i as GpmPayError, j as GpmPayNotFoundError, k as GpmPayPermissionError, l as GpmPayRateLimitError, m as GpmPayServerError, n as GpmPayTimeoutError, o as GpmPayWebhookEvent, p as GpmPayWebhookSignatureError, M as MAX_PAGE_SIZE, q as PageMeta, r as PermissionFailureReason, S as SIGNATURE_HEADER, s as SortOrder, V as VerifyWebhookInput, t as WebhookPayload, u as WebhookSignatureFailureReason, v as assertWebhookSignature, w as constructWebhookEvent, x as signWebhookPayload, y as verifyWebhookSignature } from './verify-DVgS9Vh1.cjs';
import { L as ListBankAccountsParams, B as BankAccount, a as Bank, T as TransactionType, b as TransactionSource, A as ApiTokenScope } from './bank-SVz37Xt1.cjs';
export { c as ALL_API_SCOPES, d as ApiTokenStatus, e as BankAccountStatus, W as WEBHOOK_DELIVERY_TIMEOUT_MS, f as WEBHOOK_MAX_ATTEMPTS, g as WEBHOOK_RETRY_SCHEDULE_SECONDS, h as WebhookAuthType, i as WebhookDeliveryStatus, j as WebhookDriver, k as WebhookScope } from './bank-SVz37Xt1.cjs';
export { P as PaymentInstructions, a as PaymentRequest, V as VietQrImageInput, b as VietQrInput, c as buildPaymentInstructions, d as buildVietQrImageUrl, e as buildVietQrPayload } from './vietqr-FG15aLPN.cjs';
declare const DEFAULT_BASE_URL = "https://api.gpmpay.com";
declare const SANDBOX_BASE_URL = "https://sandbox-api.gpmpay.com";
interface RequestLogEvent {
method: string;
url: string;
requestId: string;
attempt: number;
}
interface ResponseLogEvent {
requestId: string;
status: number;
durationMs: number;
attempt: number;
}
interface GpmPayOptions {
/**
* **Required.** Your GPM Pay API token (`gpm_…`).
*
* Create one at https://app.gpmpay.com/api-tokens. This is a
* server-side secret — never ship it to a browser or mobile client.
*/
apiToken: string;
/**
* API base URL. Accepts a bare host, `host/api`, or `host/api/v1` — the SDK
* normalizes all of them. Defaults to `https://api.gpmpay.com`.
*/
baseUrl?: string;
/** Shorthand for the sandbox host. Ignored when `baseUrl` is set. */
sandbox?: boolean;
/** Per-request timeout in ms. Default 30000. */
timeoutMs?: number;
/** Retries for retryable failures. Default 2. Set 0 to disable. */
maxRetries?: number;
/** Base backoff in ms; full-jitter exponential capped at 8s. Default 500. */
retryBaseDelayMs?: number;
/** Inject a fetch implementation (tests, proxy agents, instrumentation). */
fetch?: typeof globalThis.fetch;
/** Appended to the SDK's own User-Agent. */
userAgent?: string;
/** Merged into every request. Cannot override `Authorization`. */
defaultHeaders?: Record<string, string>;
/**
* Reject tokens that don't match the documented `gpm_…` shape. Default true.
* The presence check always applies regardless of this flag.
*/
strictTokenFormat?: boolean;
/** Observability hooks. Never receive the token. */
onRequest?: (event: RequestLogEvent) => void;
onResponse?: (event: ResponseLogEvent) => void;
}
interface ResolvedConfig {
apiToken: string;
baseUrl: string;
timeoutMs: number;
maxRetries: number;
retryBaseDelayMs: number;
fetchImpl: typeof globalThis.fetch;
userAgent: string | undefined;
defaultHeaders: Record<string, string>;
onRequest: ((event: RequestLogEvent) => void) | undefined;
onResponse: ((event: ResponseLogEvent) => void) | undefined;
}
/**
* Normalize any reasonable spelling of the API host into a full versioned base.
*
* Mirrors the WooCommerce plugin's normalization so merchants can paste
* whatever they have. `http://` is preserved — localhost must not be forced
* to https.
*
* @example
* normalizeBaseUrl('api.gpmpay.com') // https://api.gpmpay.com/api/v1
* normalizeBaseUrl('https://api.gpmpay.com/') // https://api.gpmpay.com/api/v1
* normalizeBaseUrl('https://api.gpmpay.com/api') // https://api.gpmpay.com/api/v1
* normalizeBaseUrl('http://localhost:4000') // http://localhost:4000/api/v1
*/
declare function normalizeBaseUrl(input: string): string;
type QueryInput = Record<string, unknown> | undefined;
type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
interface HttpRequestOptions extends RequestOptions {
query?: QueryInput;
body?: unknown;
}
declare class HttpClient {
private readonly config;
private readonly userAgent;
constructor(config: ResolvedConfig);
get baseUrl(): string;
/** Issue a request and return the unwrapped `data` payload. */
request<T>(method: HttpMethod, path: string, options?: HttpRequestOptions): Promise<T>;
/** Issue a list request and normalize it into a `Page<T>`. */
requestList<T>(method: HttpMethod, path: string, options?: HttpRequestOptions): Promise<Page<T>>;
private send;
private waitBeforeRetry;
}
/**
* The one API-token route an API token may call.
*
* @remarks
* This class looks incomplete on purpose. `ApiTokenGuard` is fail-closed: a
* route that declares no `@ApiScopes()` rejects API tokens outright with
* `403 "This endpoint is not available to API tokens"`. Listing, creating,
* regenerating and status changes are all dashboard-session routes, so
* modelling them here would only ship methods that can never succeed.
*
* `DELETE /api-tokens/:id` is the documented exception — it carries
* `@AllowAnyApiToken()` so an integration can revoke its own token when the
* merchant disconnects. Ownership is still enforced server-side.
*
* Manage tokens at https://app.gpmpay.com/api-tokens.
*/
declare class ApiTokensResource {
private readonly http;
constructor(http: HttpClient);
/**
* Permanently delete a token. Any valid token may call this; the backend
* still requires the token being deleted to belong to the caller.
*/
remove(id: string, options?: RequestOptions): Promise<unknown>;
}
/**
* Read-only access to your bank accounts. Scope: `bank-accounts:read`.
*
* @remarks
* Write operations (`create`, `update`, `remove`, `rotate-ingest-secret`,
* `subscribe`) are intentionally **not** exposed. There is no
* `bank-accounts:write` scope, `rotate-ingest-secret` returns a plaintext
* secret, and `subscribe` spends money — none of which belongs behind a
* read-scoped token. Use the dashboard for those.
*/
declare class BankAccountsResource {
private readonly http;
constructor(http: HttpClient);
list(params?: ListBankAccountsParams, options?: RequestOptions): Promise<Page<BankAccount>>;
retrieve(id: string, options?: RequestOptions): Promise<BankAccount>;
}
/**
* The catalogue of banks GPM Pay supports.
*
* @remarks
* Scope: `bank-accounts:read`.
*
* You only need this when you want a bank *you have no account at* — to render
* a bank picker, or to build a VietQR for an external account. For your own
* accounts the NAPAS `bin` already rides along on the `bank` relation of
* `bankAccounts.list()` / `bankAccounts.retrieve()`, so no extra call is needed.
*/
declare class BanksResource {
private readonly http;
constructor(http: HttpClient);
/** List active banks. The endpoint returns a flat array, not a page. */
list(options?: RequestOptions): Promise<Bank[]>;
}
interface Transaction {
id: string;
bankAccountId: string;
type: TransactionType;
source: TransactionSource;
/** VND, serialized as a string (Prisma `Decimal`). Use `toVnd()`. */
amount: string;
/** Running balance after the transaction, when the bank reports it. */
balanceAfter: string | null;
referenceCode: string;
transferContent: string;
counterAccount: string | null;
counterName: string | null;
transactionTime: string;
createdAt: string;
bankAccount?: BankAccount;
}
interface TransactionDetail extends Transaction {
webhookHistories?: unknown[];
}
interface ListTransactionsParams extends ListParams {
bankAccountId?: string;
type?: TransactionType;
source?: TransactionSource;
}
/**
* What `POST /simulator/transactions` actually returns — the transaction plus
* the webhook deliveries it queued. The route does not return a bare
* `Transaction`; it never has.
*/
interface SimulatedTransactionResult {
transaction: Transaction;
/**
* One id per queued webhook delivery. Empty when no endpoint has
* `fireOnSimulated` enabled — the fastest way to tell a silent simulation
* from a broken one.
*/
historyIds: string[];
}
interface SimulateTransactionParams {
bankAccountId: string;
/** VND, integer, >= 1. */
amount: number;
/**
* <= 100 chars. Put your own reconciliation code in here — this is the field
* you parse back out of the webhook payload's `content`.
*/
transferContent: string;
type?: TransactionType;
/** <= 64 chars. */
referenceCode?: string;
/** <= 64 chars. */
counterAccount?: string;
/** <= 255 chars. */
counterName?: string;
transactionTime?: string | Date;
}
interface SimulatorOptions extends RequestOptions {
/**
* Simulated transactions are a development tool. The SDK refuses to call
* this against a production base URL unless you opt in explicitly.
*/
allowOnProduction?: boolean;
}
/**
* Generate fake inbound transfers for local development and integration tests.
*
* Scope: `transactions:read`. That reads oddly for a route that *creates* a
* transaction — it is a deliberate backend trade-off to avoid minting a new
* scope. Simulated transactions always carry `source: 'SIMULATED'` and only
* fan out to endpoints with `fireOnSimulated` enabled.
*/
declare class SimulatorResource {
private readonly http;
constructor(http: HttpClient);
private assertSafeEnvironment;
/**
* Create a simulated bank transaction.
*
* Returns the envelope the route actually sends — `{ transaction,
* historyIds }`, not a bare `Transaction`. Read `result.transaction` for the
* ledger row and `result.historyIds.length` to confirm a webhook was queued.
*
* `async` so guard failures reject rather than throwing synchronously.
*/
createTransaction(params: SimulateTransactionParams, options?: SimulatorOptions): Promise<SimulatedTransactionResult>;
}
declare class TransactionsResource {
private readonly http;
constructor(http: HttpClient);
/**
* List transactions across your bank accounts. Scope: `transactions:read`.
*
* @remarks
* `limit` is capped at 50 server-side. `search` matches `referenceCode` and
* `transferContent`; `startDate`/`endDate` filter on `transactionTime`.
*/
list(params?: ListTransactionsParams, options?: RequestOptions): Promise<Page<Transaction>>;
/**
* Iterate every matching transaction, fetching pages as needed.
*
* @example
* for await (const txn of client.transactions.listAll({ type: 'IN' })) {
* console.log(txn.referenceCode);
* }
*/
listAll(params?: Omit<ListTransactionsParams, 'page'>, options?: RequestOptions): AsyncGenerator<Transaction, void, undefined>;
/** Fetch one transaction. Scope: `transactions:read`. */
retrieve(id: string, options?: RequestOptions): Promise<TransactionDetail>;
}
/** Inspect and replay webhook deliveries. Scope: `webhooks:manage`. */
declare class WebhookHistoriesResource {
private readonly http;
constructor(http: HttpClient);
list(params?: ListWebhookHistoriesParams, options?: RequestOptions): Promise<Page<WebhookHistory>>;
retrieve(id: string, options?: RequestOptions): Promise<WebhookHistory>;
/** Re-enqueue a failed delivery. */
retry(id: string, options?: RequestOptions): Promise<WebhookHistory>;
}
interface CreateHmacEndpointParams {
/** Where GPM Pay should POST transaction events. */
url: string;
name?: string;
scope?: 'ALL' | 'SPECIFIC';
bankAccountIds?: string[];
/** Provide your own, or let the SDK generate a 256-bit hex secret. */
secret?: string;
}
interface CreateHmacEndpointResult {
setting: WebhookSetting;
/**
* The signing secret, in plaintext.
*
* @remarks
* Returned **once**. The API stores it encrypted and never gives it back —
* persist it now, e.g. as `GPMPAY_WEBHOOK_SECRET`.
*/
secret: string;
}
/** Manage webhook endpoints. Scope: `webhooks:manage`. */
declare class WebhookSettingsResource {
private readonly http;
constructor(http: HttpClient);
list(params?: ListWebhookSettingsParams, options?: RequestOptions): Promise<Page<WebhookSetting>>;
retrieve(id: string, options?: RequestOptions): Promise<WebhookSetting>;
create(params: CreateWebhookSettingParams, options?: RequestOptions): Promise<WebhookSetting>;
update(id: string, params: UpdateWebhookSettingParams, options?: RequestOptions): Promise<WebhookSetting>;
remove(id: string, options?: RequestOptions): Promise<unknown>;
/**
* Register an HTTP endpoint with HMAC signing and hand back the secret.
*
* Pair the returned secret with `verifyWebhookSignature` from
* `@gpmpay/sdk/webhooks`.
*/
createHmacEndpoint(params: CreateHmacEndpointParams, options?: RequestOptions): Promise<CreateHmacEndpointResult>;
}
interface PingResult {
ok: true;
baseUrl: string;
/** The token's public prefix — safe to print. */
tokenPrefix: string;
latencyMs: number;
/**
* Which scopes the token actually carries, determined by probing one cheap
* read per scope. A scope lands in `denied` only on a genuine 403; any other
* failure (network, 5xx) propagates instead of being reported as missing.
*/
scopes: {
granted: ApiTokenScope[];
denied: ApiTokenScope[];
};
}
/**
* GPM Pay API client.
*
* An API token is **required** — the constructor throws immediately if one is
* missing or malformed, rather than deferring to a 401 on the first call.
*
* @example
* import { GpmPay } from '@gpmpay/sdk';
*
* const client = new GpmPay({ apiToken: process.env.GPMPAY_API_TOKEN! });
* const page = await client.transactions.list({ limit: 10 });
* console.log(page.data[0]?.transferContent);
*/
declare class GpmPay {
readonly transactions: TransactionsResource;
readonly bankAccounts: BankAccountsResource;
readonly banks: BanksResource;
readonly webhookSettings: WebhookSettingsResource;
readonly webhookHistories: WebhookHistoriesResource;
readonly apiTokens: ApiTokensResource;
readonly simulator: SimulatorResource;
private readonly http;
private readonly token;
/** @throws {GpmPayConfigError} when `apiToken` is missing or malformed. */
constructor(options: GpmPayOptions);
/**
* Build a client from `GPMPAY_API_TOKEN` and `GPMPAY_API_URL`.
*
* @throws {GpmPayConfigError} when `GPMPAY_API_TOKEN` is not set.
*/
static fromEnv(overrides?: Partial<GpmPayOptions>): GpmPay;
/** Fully normalized API base, e.g. `https://api.gpmpay.com/api/v1`. */
get baseUrl(): string;
/** First 12 chars of the token. Safe to log and quote in support tickets. */
get tokenPrefix(): string;
toString(): string;
/**
* Verify connectivity and authentication, and report the token's real scopes.
*
* Issues one minimal read per scope in parallel and infers the grant from the
* outcome. The token's own record cannot be used for this: `GET /api-tokens`
* declares no `@ApiScopes()`, so the fail-closed guard rejects every API
* token that asks for it.
*
* A 401 from the first probe to complete propagates — an invalid token is a
* hard failure, not "no scopes".
*/
ping(options?: RequestOptions): Promise<PingResult>;
/**
* Escape hatch for endpoints this SDK does not model yet.
*
* @example
* await client.request('GET', '/some/new/endpoint');
*/
request<T = unknown>(method: HttpMethod, path: string, options?: RequestOptions & {
query?: Record<string, unknown>;
body?: unknown;
}): Promise<T>;
}
/** Mirrors `API_TOKEN_PREFIX` in `apps/backend/src/modules/api-tokens/api-tokens.service.ts`. */
declare const API_TOKEN_PREFIX = "gpm_";
/**
* `gpm_` (4) + base64url public part (8) + `_` + base64url secret (24) = 37 chars.
*
* The width is fixed, so the separating `_` is unambiguous even though base64url
* itself may contain `_`.
*/
declare const API_TOKEN_RE: RegExp;
/**
* The token's public prefix — the first 12 chars, matching `ApiToken.tokenPrefix`
* in the database. Safe to log and to display in support tickets.
*/
declare function tokenPrefix(token: string): string;
/** Redacted form for logs and error messages. Never reveals the secret part. */
declare function maskToken(token: string): string;
/**
* Convert a money field to a number of VND.
*
* Amounts are read back as strings (Prisma `Decimal`) but written as integers.
* The SDK never coerces silently — call this when you need arithmetic.
*/
declare function toVnd(amount: string | number | null | undefined): number;
/** Format a money field as Vietnamese currency, e.g. `100.000 ₫`. */
declare function formatVnd(amount: string | number | null | undefined): string;
declare class AbortError extends Error {
readonly name = "AbortError";
constructor(message?: string);
}
export { API_TOKEN_PREFIX, API_TOKEN_RE, AbortError, ApiTokenScope, Bank, BankAccount, type CreateHmacEndpointParams, type CreateHmacEndpointResult, CreateWebhookSettingParams, DEFAULT_BASE_URL, GpmPay, type GpmPayOptions, type HttpMethod, ListBankAccountsParams, ListParams, type ListTransactionsParams, ListWebhookHistoriesParams, ListWebhookSettingsParams, Page, type PingResult, type RequestLogEvent, RequestOptions, type ResponseLogEvent, SANDBOX_BASE_URL, type SimulateTransactionParams, type SimulatedTransactionResult, type SimulatorOptions, type Transaction, type TransactionDetail, TransactionSource, TransactionType, UpdateWebhookSettingParams, WebhookHistory, WebhookSetting, formatVnd, maskToken, normalizeBaseUrl, toVnd, tokenPrefix };