UNPKG

@gpmpay/sdk

Version:

Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.

436 lines (418 loc) • 18.7 kB
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 };