@gpmpay/sdk
Version:
Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.
395 lines (389 loc) • 14.7 kB
TypeScript
import { k as WebhookScope, j as WebhookDriver, h as WebhookAuthType, B as BankAccount, i as WebhookDeliveryStatus, b as TransactionSource, A as ApiTokenScope } from './bank-SVz37Xt1.js';
/** Pagination envelope returned inside `data` by list endpoints. */
interface PageMeta {
page: number;
limit: number;
totalItems: number;
totalPages: number;
}
interface Page<T> {
data: T[];
meta: PageMeta;
}
type SortOrder = 'asc' | 'desc';
/**
* Query parameters shared by every paginated list endpoint.
*
* @remarks
* `limit` is capped at 50 server-side. The SDK clamps it client-side and warns
* once so the truncation is never silent.
*
* `startDate`/`endDate` filter on `transactionTime` for transactions and on
* `createdAt` for every other resource.
*/
interface ListParams {
page?: number;
limit?: number;
sortBy?: string;
sortOrder?: SortOrder;
search?: string;
startDate?: string | Date;
endDate?: string | Date;
/** Serialized to a JSON string — the backend runs `JSON.parse` on it. */
filters?: Record<string, unknown>;
}
/** Maximum page size the backend will honour (`BaseService.DEFAULT_LIMIT`). */
declare const MAX_PAGE_SIZE = 50;
interface RequestOptions {
signal?: AbortSignal;
timeoutMs?: number;
headers?: Record<string, string>;
}
/**
* The JSON body POSTed to your endpoint. Mirrors
* `apps/backend/src/modules/webhook-delivery/payload.builder.ts`.
*/
interface WebhookPayload {
/** Transaction id. Use this as your idempotency key — deliveries retry. */
id: string;
/** Bank code, e.g. `MB`, `BIDV`. */
gateway: string;
transactionDate: string;
accountNumber: string;
subAccount: string | null;
/**
* Transfer content, truncated to 100 chars. GPM Pay does no reconciliation —
* this is where you find the code you embedded when you showed the QR.
*/
content: string;
transferType: 'in' | 'out';
transferAmount: number;
accumulated: number | null;
/** The bank's own reference for the transfer — not a GPM Pay code. */
referenceCode: string;
source: TransactionSource;
}
interface WebhookSetting {
id: string;
userId: string;
name: string | null;
scope: WebhookScope;
driver: WebhookDriver;
url: string;
isActive: boolean;
fireOnSimulated: boolean;
authorizationType: WebhookAuthType;
/**
* Header the secret travels in.
*
* `null` means the driver default: `X-GPMPay-Signature` for `HMAC`,
* `Authorization` for `API_KEY`.
*/
authorizationHeaderName: string | null;
/**
* Ciphertext, not the secret.
*
* The API stores secrets encrypted and returns the encrypted column as-is —
* it cannot be decrypted client-side and is useless as a credential. Treat it
* as a presence flag only (`!== null` → a secret is configured) and never
* print it: `printJson` in the CLI redacts it for exactly this reason.
*/
authorizationSecret?: string | null;
telegramChatId: string | null;
telegramTopicId: string | null;
messageTemplate: string | null;
googleSheetName: string | null;
maxRetries: number;
successStatusCodeRange: string;
autoDisabledAt: string | null;
createdAt: string;
updatedAt?: string;
bankAccounts?: {
bankAccount: BankAccount;
}[];
}
interface WebhookCommonParams {
name?: string;
isActive?: boolean;
fireOnSimulated?: boolean;
authorizationType?: WebhookAuthType;
authorizationHeaderName?: string;
authorizationSecret?: string;
messageTemplate?: string;
}
type WebhookScopeParams = {
scope?: 'ALL';
} | {
scope: 'SPECIFIC';
bankAccountIds: [string, ...string[]];
};
/**
* Discriminated on `driver` so the backend's conditional validation
* (e.g. WORDPRESS requires `wpSecret`) is a compile error instead of a 400.
*/
type CreateWebhookSettingParams = WebhookCommonParams & WebhookScopeParams & ({
driver?: 'HTTP';
url: string;
} | {
driver: 'WORDPRESS';
url: string;
wpSecret: string;
} | {
driver: 'GOOGLE_SHEETS';
url: string;
googleSheetName?: string;
} | {
driver: 'TELEGRAM';
url?: string;
telegramChatId: string;
telegramBotToken?: string;
telegramTopicId?: string;
});
type UpdateWebhookSettingParams = Partial<WebhookCommonParams & {
scope: WebhookScope;
bankAccountIds: string[];
driver: WebhookDriver;
url: string;
wpSecret: string;
googleSheetName: string;
telegramChatId: string;
telegramBotToken: string;
telegramTopicId: string;
}>;
interface ListWebhookSettingsParams extends ListParams {
driver?: WebhookDriver;
}
/**
* One delivery attempt row. Mirrors the `WebhookHistory` Prisma model in
* `apps/backend/prisma/schema/webhooks.prisma`.
*
* @remarks
* There is no `deliveredAt` and no `errorMessage` column — success is
* `status === 'DELIVERED'`, and failure detail lives in `responseBody`.
*/
interface WebhookHistory {
id: string;
settingId: string;
transactionId: string;
status: WebhookDeliveryStatus;
attemptCount: number;
/** HTTP status the endpoint returned; null before the first attempt. */
responseStatus: number | null;
responseBody: string | null;
/** Round-trip time of the last attempt. */
durationMs: number | null;
/** The exact JSON body that was signed and sent. */
requestBody?: unknown;
nextAttemptAt: string | null;
createdAt: string;
updatedAt?: string;
/**
* Projected subset, present on `list()` only.
*
* `retrieve()` returns the **full** setting row here, which carries the
* encrypted secret columns — route it through `printJson` before printing.
*/
setting?: {
id: string;
url: string;
driver: WebhookDriver;
scope: WebhookScope;
};
/** Projected subset, present on `list()` only. */
transaction?: {
bankAccount?: {
accountNumber: string;
bank?: {
shortName: string;
};
};
};
}
interface ListWebhookHistoriesParams extends ListParams {
settingId?: string;
transactionId?: string;
status?: WebhookDeliveryStatus;
}
/**
* Reason an API token was rejected. Derived from the exact messages the backend
* emits in `api-tokens.service.ts` / `guards/api-token.guard.ts`.
*/
type AuthFailureReason = 'missing_bearer' | 'invalid_format' | 'invalid_token' | 'token_inactive' | 'token_expired' | 'user_inactive' | 'unknown';
/** Why a 403 came back. See {@link GpmPayPermissionError}. */
type PermissionFailureReason = 'scope' | 'endpoint' | 'ownership';
type ConfigErrorCode = 'missing_api_token' | 'invalid_api_token' | 'invalid_api_token_format' | 'invalid_base_url' | 'invalid_argument';
type WebhookSignatureFailureReason = 'missing_secret' | 'malformed_header' | 'timestamp_skew' | 'mismatch';
declare const BRAND: unique symbol;
/**
* Base class for everything this SDK throws.
*
* Generic over `code` so subclasses can narrow it (and give consumers
* autocomplete on `error.code`) without redeclaring the field.
*/
declare class GpmPayError<TCode extends string = string> extends Error {
readonly code: TCode;
/** @internal Cross-realm brand — `instanceof` breaks across CJS/ESM copies. */
readonly [BRAND]: true;
constructor(message: string, code: TCode);
/**
* Prefer this over `instanceof` when a dual CJS/ESM install could put two
* copies of the class in one process.
*/
static isGpmPayError(value: unknown): value is GpmPayError;
}
/** Bad SDK usage — thrown before any network call happens. */
declare class GpmPayConfigError extends GpmPayError<ConfigErrorCode> {
constructor(message: string, code?: ConfigErrorCode);
}
/** DNS/TCP/TLS failure. `cause` holds the original error. */
declare class GpmPayConnectionError extends GpmPayError {
/** Overrides the standard `Error.cause` so it is always populated here. */
readonly cause: unknown;
/** Node's `err.cause.code`, e.g. `ECONNREFUSED`, `ENOTFOUND`. */
readonly syscallCode: string | undefined;
constructor(message: string, cause: unknown, syscallCode?: string);
}
declare class GpmPayTimeoutError extends GpmPayError {
readonly timeoutMs: number;
constructor(message: string, timeoutMs: number);
}
declare class GpmPayWebhookSignatureError extends GpmPayError {
readonly reason: WebhookSignatureFailureReason;
constructor(message: string, reason: WebhookSignatureFailureReason);
}
interface ApiErrorContext {
status: number;
requestId: string;
rawBody: unknown;
rawMessage: string | string[];
}
/** Any non-2xx HTTP response. */
declare class GpmPayAPIError extends GpmPayError {
readonly status: number;
/** Correlation id sent as `X-GPMPay-Request-Id`. Quote it to support. */
readonly requestId: string;
readonly rawBody: unknown;
readonly rawMessage: string | string[];
constructor(message: string, ctx: ApiErrorContext, code?: string);
}
/** 400 — request body failed validation. */
declare class GpmPayBadRequestError extends GpmPayAPIError {
/** One entry per failed constraint, as produced by Nest's ValidationPipe. */
readonly validationMessages: string[];
constructor(message: string, ctx: ApiErrorContext & {
validationMessages: string[];
});
}
/** 401 — the API token was rejected. */
declare class GpmPayAuthenticationError extends GpmPayAPIError {
readonly reason: AuthFailureReason;
constructor(message: string, ctx: ApiErrorContext & {
reason: AuthFailureReason;
});
}
/**
* 403 — one of three things:
*
* - `scope` — the token exists but lacks the scope this route requires.
* - `endpoint` — the route accepts no API token at all (dashboard-only).
* - `ownership` — the resource exists but belongs to another account.
*/
declare class GpmPayPermissionError extends GpmPayAPIError {
readonly missingScope: ApiTokenScope | undefined;
readonly reason: PermissionFailureReason;
constructor(message: string, ctx: ApiErrorContext & {
missingScope?: ApiTokenScope;
reason: PermissionFailureReason;
});
}
declare class GpmPayNotFoundError extends GpmPayAPIError {
/** Resource name parsed out of e.g. `"Order not found"`. */
readonly resource: string | undefined;
constructor(message: string, ctx: ApiErrorContext & {
resource?: string;
});
}
declare class GpmPayRateLimitError extends GpmPayAPIError {
readonly retryAfterSeconds: number | undefined;
constructor(message: string, ctx: ApiErrorContext & {
retryAfterSeconds?: number;
});
}
declare class GpmPayServerError extends GpmPayAPIError {
constructor(message: string, ctx: ApiErrorContext);
}
/** Header carrying the signature. Overridable per webhook setting. */
declare const SIGNATURE_HEADER = "X-GPMPay-Signature";
/** Header carrying the event name (WordPress driver). */
declare const EVENT_HEADER = "X-GPMPay-Event";
/** Clock-skew window the backend also enforces. */
declare const DEFAULT_TOLERANCE_SECONDS = 300;
interface VerifyWebhookInput {
/**
* The **exact bytes** of the request body.
*
* Never `JSON.stringify(req.body)` — re-serializing a parsed body changes
* whitespace and key order, and the signature will never match.
*/
rawBody: string | Buffer | Uint8Array;
/** Value of `X-GPMPay-Signature`: `t=<unix_seconds>,v1=<hex_sha256>`. */
signature: string;
/**
* `authorizationSecret` for an HTTP+HMAC webhook, or `wpSecret` for the
* WordPress driver.
*/
secret: string;
/** Skew window in seconds. Default 300. Pass 0 to disable (tests only). */
toleranceSeconds?: number;
/** @internal Test seam. */
now?: () => number;
}
/**
* Verify a webhook signature, throwing a typed error explaining exactly why it
* failed.
*
* Matches `apps/backend/src/modules/webhook-delivery/hmac.util.ts`: the signed
* string is `` `${t}.${rawBody}` ``, hashed with HMAC-SHA256 and compared in
* constant time.
*
* @throws {GpmPayWebhookSignatureError}
*/
declare function assertWebhookSignature(input: VerifyWebhookInput): {
timestamp: number;
};
/** Boolean form of {@link assertWebhookSignature}. */
declare function verifyWebhookSignature(input: VerifyWebhookInput): boolean;
/**
* Produce a signature header. Useful for testing your own handler and for
* generating fixtures; GPM Pay signs real deliveries itself.
*/
declare function signWebhookPayload(params: {
rawBody: string | Buffer | Uint8Array;
secret: string;
/** Unix seconds. Defaults to now. */
timestamp?: number;
}): string;
interface GpmPayWebhookEvent {
/** From `X-GPMPay-Event`; defaults to `transaction.created`. */
type: string;
/** Unix seconds the delivery was signed at. */
timestamp: number;
payload: WebhookPayload;
/** The raw body, for logging or re-verification. */
rawBody: string;
}
/**
* Verify a delivery and parse it into a typed event.
*
* @throws {GpmPayWebhookSignatureError} when verification fails.
*/
declare function constructWebhookEvent(input: VerifyWebhookInput & {
headers?: Record<string, string | string[] | undefined>;
}): GpmPayWebhookEvent;
/**
* Constant-time comparison for webhooks configured with
* `authorizationType: 'API_KEY'`, where the raw secret is sent in a header
* instead of being used to sign the body.
*/
declare function verifyApiKeyHeader(received: string | undefined, expected: string): boolean;
export { type AuthFailureReason as A, type CreateWebhookSettingParams as C, DEFAULT_TOLERANCE_SECONDS as D, EVENT_HEADER as E, GpmPayAPIError as G, type ListParams as L, MAX_PAGE_SIZE as M, type Page as P, type RequestOptions as R, SIGNATURE_HEADER as S, type UpdateWebhookSettingParams as U, type VerifyWebhookInput as V, type WebhookHistory as W, type ListWebhookHistoriesParams as a, type WebhookSetting as b, type ListWebhookSettingsParams as c, type ConfigErrorCode as d, GpmPayAuthenticationError as e, GpmPayBadRequestError as f, GpmPayConfigError as g, GpmPayConnectionError as h, GpmPayError as i, GpmPayNotFoundError as j, GpmPayPermissionError as k, GpmPayRateLimitError as l, GpmPayServerError as m, GpmPayTimeoutError as n, type GpmPayWebhookEvent as o, GpmPayWebhookSignatureError as p, type PageMeta as q, type PermissionFailureReason as r, type SortOrder as s, type WebhookPayload as t, type WebhookSignatureFailureReason as u, assertWebhookSignature as v, constructWebhookEvent as w, signWebhookPayload as x, verifyWebhookSignature as y, verifyApiKeyHeader as z };