UNPKG

@gpmpay/sdk

Version:

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

1 lines • 35.6 kB
{"version":3,"sources":["../../src/core/errors.ts","../../src/webhooks/verify.ts","../../src/webhooks/express.ts","../../src/webhooks/next.ts","../../src/types/enums.ts"],"names":["createHmac","timingSafeEqual"],"mappings":";;;;;;;AA+BA,IAAM,KAAA,mBAAQ,MAAA,CAAO,GAAA,CAAI,kBAAkB,CAAA;AAQpC,IAAM,WAAA,GAAN,cAAyD,KAAA,CAAM;AAAA,EAC3D,IAAA;AAAA;AAAA,EAET,CAAU,KAAK,IAAI,IAAA;AAAA,EAEnB,WAAA,CAAY,SAAiB,IAAA,EAAa;AACxC,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,OAAO,GAAA,CAAA,MAAA,CAAW,IAAA;AACvB,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,GAAA,CAAA,MAAA,CAAW,SAAS,CAAA;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,cAAc,KAAA,EAAsC;AACzD,IAAA,OACE,OAAO,KAAA,KAAU,QAAA,IACjB,UAAU,IAAA,IACT,KAAA,CAAkC,KAAK,CAAA,KAAM,IAAA;AAAA,EAElD;AACF,CAAA;AAGO,IAAM,iBAAA,GAAN,cAAgC,WAAA,CAA6B;AAAA,EAClE,WAAA,CAAY,OAAA,EAAiB,IAAA,GAAwB,kBAAA,EAAoB;AACvE,IAAA,KAAA,CAAM,SAAS,IAAI,CAAA;AAAA,EACrB;AACF,CAAA;AAyBO,IAAM,2BAAA,GAAN,cAA0C,WAAA,CAAY;AAAA,EAClD,MAAA;AAAA,EAET,WAAA,CAAY,SAAiB,MAAA,EAAuC;AAClE,IAAA,KAAA,CAAM,SAAS,mBAAmB,CAAA;AAClC,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AACF;;;AC/FO,IAAM,gBAAA,GAAmB;AAGzB,IAAM,YAAA,GAAe;AAGrB,IAAM,yBAAA,GAA4B;AAuBzC,SAAS,SAAS,KAAA,EAA6C;AAC7D,EAAA,IAAI,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAG,OAAO,KAAA;AACnC,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,SAAiB,MAAA,CAAO,IAAA,CAAK,OAAO,MAAM,CAAA;AAC/D,EAAA,OAAO,MAAA,CAAO,KAAK,KAAK,CAAA;AAC1B;AAMA,SAAS,qBAAqB,SAAA,EAA2C;AACvE,EAAA,MAAM,QAAgC,EAAC;AACvC,EAAA,KAAA,MAAW,OAAA,IAAW,SAAA,CAAU,KAAA,CAAM,GAAG,CAAA,EAAG;AAC1C,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACjC,IAAA,IAAI,QAAQ,CAAA,EAAG;AACb,MAAA,KAAA,CAAM,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,KAAK,CAAA,CAAE,IAAA,EAAM,CAAA,GAAI,OAAA,CAAQ,KAAA,CAAM,KAAA,GAAQ,CAAC,EAAE,IAAA,EAAK;AAAA,IACxE;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AAYO,SAAS,uBAAuB,KAAA,EAErC;AACA,EAAA,MAAM,EAAE,SAAA,EAAW,MAAA,EAAQ,GAAA,GAAM,IAAA,CAAK,KAAI,GAAI,KAAA;AAC9C,EAAA,MAAM,SAAA,GAAY,MAAM,gBAAA,IAAoB,yBAAA;AAE5C,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,KAAW,EAAA,EAAI;AAC/C,IAAA,MAAM,IAAI,2BAAA;AAAA,MACR,0HAAA;AAAA,MAEA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,IAAI,OAAO,SAAA,KAAc,QAAA,IAAY,SAAA,CAAU,IAAA,OAAW,EAAA,EAAI;AAC5D,IAAA,MAAM,IAAI,2BAAA;AAAA,MACR,WAAW,gBAAgB,CAAA,QAAA,CAAA;AAAA,MAC3B;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,qBAAqB,SAAS,CAAA;AAC5C,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA;AAChC,EAAA,MAAM,KAAK,KAAA,CAAM,EAAA;AAEjB,EAAA,IAAI,CAAC,OAAO,QAAA,CAAS,SAAS,KAAK,SAAA,IAAa,CAAA,IAAK,CAAC,EAAA,EAAI;AACxD,IAAA,MAAM,IAAI,2BAAA;AAAA,MACR,CAAA,UAAA,EAAa,gBAAgB,CAAA,GAAA,EAAM,SAAS,CAAA,wCAAA,CAAA;AAAA,MAC5C;AAAA,KACF;AAAA,EACF;AAEA,EAAA,IAAI,SAAA,GAAY,KAAK,IAAA,CAAK,GAAA,CAAI,KAAI,GAAI,GAAA,GAAO,SAAS,CAAA,GAAI,SAAA,EAAW;AACnE,IAAA,MAAM,IAAI,2BAAA;AAAA,MACR,oCAAoC,MAAA,CAAO,SAAS,CAAC,CAAA,eAAA,EAAkB,MAAA,CAAO,SAAS,CAAC,CAAA,wDAAA,CAAA;AAAA,MAExF;AAAA,KACF;AAAA,EACF;AAIA,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,CAAM,OAAO,CAAA;AAClC,EAAA,MAAM,QAAA,GAAWA,kBAAW,QAAA,EAAU,MAAM,EACzC,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,CAAC,MAAA,CAAO,IAAA,CAAK,GAAG,MAAA,CAAO,SAAS,CAAC,CAAA,CAAA,CAAA,EAAK,MAAM,GAAG,GAAG,CAAC,CAAC,CAAA,CACzE,MAAA,EAAO;AAEV,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,IAAA,CAAK,EAAA,EAAI,KAAK,CAAA;AACtC,EAAA,IACE,QAAA,CAAS,WAAW,QAAA,CAAS,MAAA,IAC7B,CAACC,sBAAA,CAAgB,QAAA,EAAU,QAAQ,CAAA,EACnC;AACA,IAAA,MAAM,IAAI,2BAAA;AAAA,MACR,oKAAA;AAAA,MAEA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,SAAA,EAAU;AACrB;AAGO,SAAS,uBAAuB,KAAA,EAAoC;AACzE,EAAA,IAAI;AACF,IAAA,sBAAA,CAAuB,KAAK,CAAA;AAC5B,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAMO,SAAS,mBAAmB,MAAA,EAKxB;AACT,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA,KAAQ,GAAI,CAAA;AAClE,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,MAAA,CAAO,OAAO,CAAA;AACnC,EAAA,MAAM,MAAA,GAASD,iBAAA,CAAW,QAAA,EAAU,MAAA,CAAO,MAAM,EAC9C,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,CAAC,MAAA,CAAO,IAAA,CAAK,GAAG,MAAA,CAAO,SAAS,CAAC,CAAA,CAAA,CAAA,EAAK,MAAM,CAAA,EAAG,GAAG,CAAC,CAAC,CAAA,CACzE,MAAA,CAAO,KAAK,CAAA;AACf,EAAA,OAAO,CAAA,EAAA,EAAK,MAAA,CAAO,SAAS,CAAC,OAAO,MAAM,CAAA,CAAA;AAC5C;AAYA,SAAS,WAAA,CACP,SACA,IAAA,EACoB;AACpB,EAAA,IAAI,CAAC,SAAS,OAAO,MAAA;AACrB,EAAA,MAAM,MAAA,GAAS,KAAK,WAAA,EAAY;AAChC,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,IAAA,IAAI,GAAA,CAAI,WAAA,EAAY,KAAM,MAAA,EAAQ;AAClC,IAAA,OAAO,MAAM,OAAA,CAAQ,KAAK,CAAA,GAAI,KAAA,CAAM,CAAC,CAAA,GAAI,KAAA;AAAA,EAC3C;AACA,EAAA,OAAO,MAAA;AACT;AAOO,SAAS,sBACd,KAAA,EAGoB;AACpB,EAAA,MAAM,EAAE,SAAA,EAAU,GAAI,sBAAA,CAAuB,KAAK,CAAA;AAClD,EAAA,MAAM,UAAU,QAAA,CAAS,KAAA,CAAM,OAAO,CAAA,CAAE,SAAS,MAAM,CAAA;AAEvD,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,WAAA,CAAY,KAAA,CAAM,OAAA,EAAS,YAAY,CAAA,IAAK,qBAAA;AAAA,IAClD,SAAA;AAAA,IACA,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAAA,IAC3B;AAAA,GACF;AACF;AAOO,SAAS,kBAAA,CACd,UACA,QAAA,EACS;AACT,EAAA,IAAI,OAAO,QAAA,KAAa,QAAA,IAAY,QAAA,KAAa,IAAI,OAAO,KAAA;AAC5D,EAAA,MAAM,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,QAAA,EAAU,MAAM,CAAA;AACtC,EAAA,MAAM,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,QAAA,EAAU,MAAM,CAAA;AACtC,EAAA,IAAI,CAAA,CAAE,MAAA,KAAW,CAAA,CAAE,MAAA,EAAQ,OAAO,KAAA;AAClC,EAAA,OAAOC,sBAAA,CAAgB,GAAG,CAAC,CAAA;AAC7B;;;ACxKA,IAAM,gBAAA,GACJ,miBAAA;AAUF,SAAS,eAAe,GAAA,EAAsC;AAC5D,EAAA,IAAI,OAAO,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA,SAAU,GAAA,CAAI,OAAA;AAC7C,EAAA,IAAI,OAAO,GAAA,CAAI,OAAA,KAAY,QAAA,SAAiB,GAAA,CAAI,OAAA;AAChD,EAAA,IAAI,OAAO,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA,SAAU,GAAA,CAAI,IAAA;AAC1C,EAAA,IAAI,OAAO,GAAA,CAAI,IAAA,KAAS,QAAA,SAAiB,GAAA,CAAI,IAAA;AAE7C,EAAA,MAAM,IAAI,kBAAkB,gBAAgB,CAAA;AAC9C;AAoBO,SAAS,cAAc,OAAA,EAA+B;AAC3D,EAAA,MAAM;AAAA,IACJ,MAAA;AAAA,IACA,OAAA;AAAA,IACA,gBAAA;AAAA,IACA,UAAA,GAAa,gBAAA;AAAA,IACb,OAAA;AAAA,IACA,YAAA,GAAe;AAAA,GACjB,GAAI,OAAA;AAEJ,EAAA,OAAO,eAAe,OAAA,CACpB,GAAA,EACA,GAAA,EACA,IAAA,EACe;AACf,IAAA,IAAI,KAAA;AAEJ,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,GAAU,eAAe,GAAG,CAAA;AAClC,MAAA,MAAM,SAAA,GAAY,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,OAAO,CAAA,CAAE,IAAA;AAAA,QACzC,CAAC,GAAA,KAAQ,GAAA,CAAI,WAAA,EAAY,KAAM,WAAW,WAAA;AAAY,OACxD;AACA,MAAA,MAAM,YAAY,SAAA,KAAc,KAAA,CAAA,GAAY,KAAA,CAAA,GAAY,GAAA,CAAI,QAAQ,SAAS,CAAA;AAC7E,MAAA,MAAM,YAAY,KAAA,CAAM,OAAA,CAAQ,SAAS,CAAA,GAAI,SAAA,CAAU,CAAC,CAAA,GAAI,SAAA;AAE5D,MAAA,KAAA,GAAQ,qBAAA,CAAsB;AAAA,QAC5B,OAAA;AAAA,QACA,WAAW,SAAA,IAAa,EAAA;AAAA,QACxB,MAAA;AAAA,QACA,SAAS,GAAA,CAAI,OAAA;AAAA,QACb,GAAI,gBAAA,KAAqB,KAAA,CAAA,GAAY,EAAC,GAAI,EAAE,gBAAA;AAAiB,OAC9D,CAAA;AAAA,IACH,SAAS,KAAA,EAAO;AACd,MAAA,IAAI,iBAAiB,2BAAA,EAA6B;AAChD,QAAA,OAAA,GAAU,OAAO,GAAG,CAAA;AACpB,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,KAAA,EAAO,KAAA,CAAM,OAAA,EAAS,MAAA,EAAQ,KAAA,CAAM,MAAA,EAAQ,CAAA;AACnE,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,KAAK,CAAA;AACV,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AACvC,MAAA,IAAI;AACF,QAAA,MAAM,OAAA,CAAQ,OAAO,GAAG,CAAA;AAAA,MAC1B,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,KAAK,CAAA;AAAA,MACZ;AACA,MAAA;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,CAAQ,OAAO,GAAG,CAAA;AACxB,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AAAA,IACzC,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,KAAK,CAAA;AAAA,IACZ;AAAA,EACF,CAAA;AACF;;;ACzHA,eAAsB,iBAAA,CACpB,SACA,OAAA,EAC6B;AAC7B,EAAA,MAAM,EAAE,MAAA,EAAQ,gBAAA,EAAkB,UAAA,GAAa,kBAAiB,GAAI,OAAA;AAEpE,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,IAAA,EAAK;AACnC,EAAA,MAAM,UAAkC,EAAC;AACzC,EAAA,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAA,KAAQ;AACtC,IAAA,OAAA,CAAQ,GAAG,CAAA,GAAI,KAAA;AAAA,EACjB,CAAC,CAAA;AAED,EAAA,OAAO,qBAAA,CAAsB;AAAA,IAC3B,OAAA;AAAA,IACA,SAAA,EAAW,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA,IAAK,EAAA;AAAA,IAC9C,MAAA;AAAA,IACA,OAAA;AAAA,IACA,GAAI,gBAAA,KAAqB,MAAA,GAAY,EAAC,GAAI,EAAE,gBAAA;AAAiB,GAC9D,CAAA;AACH;AAkBO,SAAS,yBACd,OAAA,EAGyC;AACzC,EAAA,MAAM,EAAE,OAAA,EAAS,GAAG,aAAA,EAAc,GAAI,OAAA;AAEtC,EAAA,OAAO,eAAe,KAAK,OAAA,EAAqC;AAC9D,IAAA,IAAI,KAAA;AACJ,IAAA,IAAI;AACF,MAAA,KAAA,GAAQ,MAAM,iBAAA,CAAkB,OAAA,EAAS,aAAa,CAAA;AAAA,IACxD,SAAS,KAAA,EAAO;AACd,MAAA,IAAI,iBAAiB,2BAAA,EAA6B;AAChD,QAAA,OAAO,QAAA,CAAS,IAAA;AAAA,UACd,EAAE,KAAA,EAAO,KAAA,CAAM,OAAA,EAAS,MAAA,EAAQ,MAAM,MAAA,EAAO;AAAA,UAC7C,EAAE,QAAQ,GAAA;AAAI,SAChB;AAAA,MACF;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAEA,IAAA,MAAM,QAAQ,KAAK,CAAA;AACnB,IAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,QAAA,EAAU,MAAM,CAAA;AAAA,EACzC,CAAA;AACF;AAYA,eAAsB,WAAA,CACpB,MAAA,EACA,QAAA,GAAW,IAAA,GAAO,IAAA,EACD;AACjB,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,IAAI,KAAA,GAAQ,CAAA;AAEZ,EAAA,WAAA,MAAiB,SAAS,MAAA,EAAQ;AAChC,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA;AAC7B,IAAA,KAAA,IAAS,GAAA,CAAI,MAAA;AACb,IAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,sBAAA,EAAyB,MAAA,CAAO,QAAQ,CAAC,CAAA,gCAAA;AAAA,OAC3C;AAAA,IACF;AACA,IAAA,MAAA,CAAO,KAAK,GAAG,CAAA;AAAA,EACjB;AAEA,EAAA,OAAO,MAAA,CAAO,OAAO,MAAM,CAAA;AAC7B;;;AC7DO,IAAM,8BAAA,GAAoD;AAAA,EAC/D,EAAA;AAAA,EAAI,EAAA;AAAA,EAAI,GAAA;AAAA,EAAK,GAAA;AAAA,EAAK,IAAA;AAAA,EAAM;AAC1B;AAEO,IAAM,oBAAA,GAAuB;AAM7B,IAAM,2BAAA,GAA8B","file":"index.cjs","sourcesContent":["import type { ApiTokenScope } from '../types/enums.js';\n\n/**\n * Reason an API token was rejected. Derived from the exact messages the backend\n * emits in `api-tokens.service.ts` / `guards/api-token.guard.ts`.\n */\nexport type AuthFailureReason =\n | 'missing_bearer'\n | 'invalid_format'\n | 'invalid_token'\n | 'token_inactive'\n | 'token_expired'\n | 'user_inactive'\n | 'unknown';\n\n/** Why a 403 came back. See {@link GpmPayPermissionError}. */\nexport type PermissionFailureReason = 'scope' | 'endpoint' | 'ownership';\n\nexport type ConfigErrorCode =\n | 'missing_api_token'\n | 'invalid_api_token'\n | 'invalid_api_token_format'\n | 'invalid_base_url'\n | 'invalid_argument';\n\nexport type WebhookSignatureFailureReason =\n | 'missing_secret'\n | 'malformed_header'\n | 'timestamp_skew'\n | 'mismatch';\n\nconst BRAND = Symbol.for('gpmpay.sdk.error');\n\n/**\n * Base class for everything this SDK throws.\n *\n * Generic over `code` so subclasses can narrow it (and give consumers\n * autocomplete on `error.code`) without redeclaring the field.\n */\nexport class GpmPayError<TCode extends string = string> extends Error {\n readonly code: TCode;\n /** @internal Cross-realm brand — `instanceof` breaks across CJS/ESM copies. */\n readonly [BRAND] = true as const;\n\n constructor(message: string, code: TCode) {\n super(message);\n this.name = new.target.name;\n this.code = code;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n /**\n * Prefer this over `instanceof` when a dual CJS/ESM install could put two\n * copies of the class in one process.\n */\n static isGpmPayError(value: unknown): value is GpmPayError {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[BRAND] === true\n );\n }\n}\n\n/** Bad SDK usage — thrown before any network call happens. */\nexport class GpmPayConfigError extends GpmPayError<ConfigErrorCode> {\n constructor(message: string, code: ConfigErrorCode = 'invalid_argument') {\n super(message, code);\n }\n}\n\n/** DNS/TCP/TLS failure. `cause` holds the original error. */\nexport class GpmPayConnectionError extends GpmPayError {\n /** Overrides the standard `Error.cause` so it is always populated here. */\n override readonly cause: unknown;\n /** Node's `err.cause.code`, e.g. `ECONNREFUSED`, `ENOTFOUND`. */\n readonly syscallCode: string | undefined;\n\n constructor(message: string, cause: unknown, syscallCode?: string) {\n super(message, 'connection_error');\n this.cause = cause;\n this.syscallCode = syscallCode;\n }\n}\n\nexport class GpmPayTimeoutError extends GpmPayError {\n readonly timeoutMs: number;\n\n constructor(message: string, timeoutMs: number) {\n super(message, 'timeout');\n this.timeoutMs = timeoutMs;\n }\n}\n\nexport class GpmPayWebhookSignatureError extends GpmPayError {\n readonly reason: WebhookSignatureFailureReason;\n\n constructor(message: string, reason: WebhookSignatureFailureReason) {\n super(message, 'webhook_signature');\n this.reason = reason;\n }\n}\n\nexport interface ApiErrorContext {\n status: number;\n requestId: string;\n rawBody: unknown;\n rawMessage: string | string[];\n}\n\n/** Any non-2xx HTTP response. */\nexport class GpmPayAPIError extends GpmPayError {\n readonly status: number;\n /** Correlation id sent as `X-GPMPay-Request-Id`. Quote it to support. */\n readonly requestId: string;\n readonly rawBody: unknown;\n readonly rawMessage: string | string[];\n\n constructor(message: string, ctx: ApiErrorContext, code = 'api_error') {\n super(message, code);\n this.status = ctx.status;\n this.requestId = ctx.requestId;\n this.rawBody = ctx.rawBody;\n this.rawMessage = ctx.rawMessage;\n }\n}\n\n/** 400 — request body failed validation. */\nexport class GpmPayBadRequestError extends GpmPayAPIError {\n /** One entry per failed constraint, as produced by Nest's ValidationPipe. */\n readonly validationMessages: string[];\n\n constructor(\n message: string,\n ctx: ApiErrorContext & { validationMessages: string[] },\n ) {\n super(message, ctx, 'bad_request');\n this.validationMessages = ctx.validationMessages;\n }\n}\n\n/** 401 — the API token was rejected. */\nexport class GpmPayAuthenticationError extends GpmPayAPIError {\n readonly reason: AuthFailureReason;\n\n constructor(message: string, ctx: ApiErrorContext & { reason: AuthFailureReason }) {\n super(message, ctx, 'authentication_error');\n this.reason = ctx.reason;\n }\n}\n\n/**\n * 403 — one of three things:\n *\n * - `scope` — the token exists but lacks the scope this route requires.\n * - `endpoint` — the route accepts no API token at all (dashboard-only).\n * - `ownership` — the resource exists but belongs to another account.\n */\nexport class GpmPayPermissionError extends GpmPayAPIError {\n readonly missingScope: ApiTokenScope | undefined;\n readonly reason: PermissionFailureReason;\n\n constructor(\n message: string,\n ctx: ApiErrorContext & {\n missingScope?: ApiTokenScope;\n reason: PermissionFailureReason;\n },\n ) {\n super(message, ctx, 'permission_error');\n this.missingScope = ctx.missingScope;\n this.reason = ctx.reason;\n }\n}\n\nexport class GpmPayNotFoundError extends GpmPayAPIError {\n /** Resource name parsed out of e.g. `\"Order not found\"`. */\n readonly resource: string | undefined;\n\n constructor(message: string, ctx: ApiErrorContext & { resource?: string }) {\n super(message, ctx, 'not_found');\n this.resource = ctx.resource;\n }\n}\n\nexport class GpmPayRateLimitError extends GpmPayAPIError {\n readonly retryAfterSeconds: number | undefined;\n\n constructor(\n message: string,\n ctx: ApiErrorContext & { retryAfterSeconds?: number },\n ) {\n super(message, ctx, 'rate_limit');\n this.retryAfterSeconds = ctx.retryAfterSeconds;\n }\n}\n\nexport class GpmPayServerError extends GpmPayAPIError {\n constructor(message: string, ctx: ApiErrorContext) {\n super(message, ctx, 'server_error');\n }\n}\n\n/** Ordered most-specific first — `Invalid token` must lose to `Invalid token format`. */\nconst AUTH_REASONS: [RegExp, AuthFailureReason][] = [\n [/missing bearer token/i, 'missing_bearer'],\n [/invalid token format/i, 'invalid_format'],\n [/token is not active/i, 'token_inactive'],\n [/token[_ ]expired/i, 'token_expired'],\n [/user inactive/i, 'user_inactive'],\n [/invalid token/i, 'invalid_token'],\n];\n\nfunction extractMessage(body: unknown): string | string[] {\n if (typeof body === 'string' && body.trim() !== '') return body;\n if (body !== null && typeof body === 'object') {\n const b = body as { message?: unknown; error?: unknown };\n if (Array.isArray(b.message) || typeof b.message === 'string') {\n return b.message as string | string[];\n }\n if (typeof b.error === 'string') return b.error;\n }\n return '';\n}\n\n/**\n * Map an HTTP error response onto the typed error hierarchy.\n *\n * @remarks\n * The backend registers no global exception filter, so error bodies use Nest's\n * default shape (`{ statusCode, message, error }`) and are NOT wrapped in the\n * `{ statusCode, message, data }` success envelope.\n */\nexport function errorFromResponse(\n status: number,\n body: unknown,\n requestId: string,\n extra: { retryAfterSeconds?: number } = {},\n): GpmPayAPIError {\n const raw = extractMessage(body);\n const msg =\n (Array.isArray(raw) ? raw.join('; ') : raw) || `HTTP ${String(status)}`;\n const ctx: ApiErrorContext = {\n status,\n requestId,\n rawBody: body,\n rawMessage: raw === '' ? msg : raw,\n };\n\n switch (status) {\n case 400:\n return new GpmPayBadRequestError(msg, {\n ...ctx,\n validationMessages: Array.isArray(raw) ? raw : [msg],\n });\n\n case 401: {\n const reason =\n AUTH_REASONS.find(([re]) => re.test(msg))?.[1] ?? 'unknown';\n return new GpmPayAuthenticationError(\n `${msg} — check your API token is correct, ACTIVE and not expired.`,\n { ...ctx, reason },\n );\n }\n\n case 403: {\n const scope = /missing scope:\\s*(\\S+)/i.exec(msg)?.[1];\n if (scope) {\n return new GpmPayPermissionError(\n `API token is missing the \"${scope}\" scope. Regenerate the token with that scope enabled.`,\n { ...ctx, missingScope: scope as ApiTokenScope, reason: 'scope' },\n );\n }\n // `ApiTokenGuard` is fail-closed: a route that declares no `@ApiScopes()`\n // rejects API tokens outright, whoever owns the resource. Reporting that\n // as an ownership problem sends people hunting for the wrong bug.\n if (/not available to api tokens/i.test(msg)) {\n return new GpmPayPermissionError(\n `${msg} — this endpoint is dashboard-only and no API token scope grants it.`,\n { ...ctx, reason: 'endpoint' },\n );\n }\n return new GpmPayPermissionError(\n `${msg} — the resource exists but belongs to another account.`,\n { ...ctx, reason: 'ownership' },\n );\n }\n\n case 404: {\n const resource = /^(.*?)\\s+not found/i.exec(msg)?.[1];\n return new GpmPayNotFoundError(\n msg,\n resource === undefined ? ctx : { ...ctx, resource },\n );\n }\n\n // No 409 case: every route this SDK can reach is either a read or a write\n // with no uniqueness constraint, so a conflict has no source. A 409 from\n // `client.request()` falls through to the generic GpmPayAPIError below.\n\n case 429:\n return new GpmPayRateLimitError(\n msg,\n extra.retryAfterSeconds === undefined\n ? ctx\n : { ...ctx, retryAfterSeconds: extra.retryAfterSeconds },\n );\n\n default:\n return status >= 500\n ? new GpmPayServerError(msg, ctx)\n : new GpmPayAPIError(msg, ctx);\n }\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\nimport { GpmPayWebhookSignatureError } from '../core/errors.js';\nimport type { WebhookPayload } from '../types/webhook.js';\n\n/** Header carrying the signature. Overridable per webhook setting. */\nexport const SIGNATURE_HEADER = 'X-GPMPay-Signature';\n\n/** Header carrying the event name (WordPress driver). */\nexport const EVENT_HEADER = 'X-GPMPay-Event';\n\n/** Clock-skew window the backend also enforces. */\nexport const DEFAULT_TOLERANCE_SECONDS = 300;\n\nexport interface VerifyWebhookInput {\n /**\n * The **exact bytes** of the request body.\n *\n * Never `JSON.stringify(req.body)` — re-serializing a parsed body changes\n * whitespace and key order, and the signature will never match.\n */\n rawBody: string | Buffer | Uint8Array;\n /** Value of `X-GPMPay-Signature`: `t=<unix_seconds>,v1=<hex_sha256>`. */\n signature: string;\n /**\n * `authorizationSecret` for an HTTP+HMAC webhook, or `wpSecret` for the\n * WordPress driver.\n */\n secret: string;\n /** Skew window in seconds. Default 300. Pass 0 to disable (tests only). */\n toleranceSeconds?: number;\n /** @internal Test seam. */\n now?: () => number;\n}\n\nfunction toBuffer(input: string | Buffer | Uint8Array): Buffer {\n if (Buffer.isBuffer(input)) return input;\n if (typeof input === 'string') return Buffer.from(input, 'utf8');\n return Buffer.from(input);\n}\n\n/**\n * Parse `t=…,v1=…`, splitting each pair on its **first** `=` only — hex and\n * base64 values may legitimately contain `=`.\n */\nfunction parseSignatureHeader(signature: string): Record<string, string> {\n const parts: Record<string, string> = {};\n for (const segment of signature.split(',')) {\n const index = segment.indexOf('=');\n if (index > 0) {\n parts[segment.slice(0, index).trim()] = segment.slice(index + 1).trim();\n }\n }\n return parts;\n}\n\n/**\n * Verify a webhook signature, throwing a typed error explaining exactly why it\n * failed.\n *\n * Matches `apps/backend/src/modules/webhook-delivery/hmac.util.ts`: the signed\n * string is `` `${t}.${rawBody}` ``, hashed with HMAC-SHA256 and compared in\n * constant time.\n *\n * @throws {GpmPayWebhookSignatureError}\n */\nexport function assertWebhookSignature(input: VerifyWebhookInput): {\n timestamp: number;\n} {\n const { signature, secret, now = Date.now } = input;\n const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;\n\n if (typeof secret !== 'string' || secret === '') {\n throw new GpmPayWebhookSignatureError(\n 'Webhook secret is empty. Pass the secret you configured on the webhook ' +\n 'setting (e.g. process.env.GPMPAY_WEBHOOK_SECRET).',\n 'missing_secret',\n );\n }\n\n if (typeof signature !== 'string' || signature.trim() === '') {\n throw new GpmPayWebhookSignatureError(\n `Missing ${SIGNATURE_HEADER} header.`,\n 'malformed_header',\n );\n }\n\n const parts = parseSignatureHeader(signature);\n const timestamp = Number(parts.t);\n const v1 = parts.v1;\n\n if (!Number.isFinite(timestamp) || timestamp <= 0 || !v1) {\n throw new GpmPayWebhookSignatureError(\n `Malformed ${SIGNATURE_HEADER}: \"${signature}\". Expected \"t=<unix_seconds>,v1=<hex>\".`,\n 'malformed_header',\n );\n }\n\n if (tolerance > 0 && Math.abs(now() / 1000 - timestamp) > tolerance) {\n throw new GpmPayWebhookSignatureError(\n `Webhook timestamp is outside the ${String(tolerance)}s tolerance (t=${String(timestamp)}). ` +\n 'Check for clock skew between your server and GPM Pay.',\n 'timestamp_skew',\n );\n }\n\n // Hash over bytes, not characters: Vietnamese transfer contents are\n // multi-byte, and hashing the string length would silently diverge.\n const raw = toBuffer(input.rawBody);\n const expected = createHmac('sha256', secret)\n .update(Buffer.concat([Buffer.from(`${String(timestamp)}.`, 'utf8'), raw]))\n .digest();\n\n const provided = Buffer.from(v1, 'hex');\n if (\n expected.length !== provided.length ||\n !timingSafeEqual(expected, provided)\n ) {\n throw new GpmPayWebhookSignatureError(\n 'Webhook signature mismatch — wrong secret, or the body was modified in transit. ' +\n 'Make sure you are verifying the raw request body, not a re-serialized object.',\n 'mismatch',\n );\n }\n\n return { timestamp };\n}\n\n/** Boolean form of {@link assertWebhookSignature}. */\nexport function verifyWebhookSignature(input: VerifyWebhookInput): boolean {\n try {\n assertWebhookSignature(input);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Produce a signature header. Useful for testing your own handler and for\n * generating fixtures; GPM Pay signs real deliveries itself.\n */\nexport function signWebhookPayload(params: {\n rawBody: string | Buffer | Uint8Array;\n secret: string;\n /** Unix seconds. Defaults to now. */\n timestamp?: number;\n}): string {\n const timestamp = params.timestamp ?? Math.floor(Date.now() / 1000);\n const raw = toBuffer(params.rawBody);\n const digest = createHmac('sha256', params.secret)\n .update(Buffer.concat([Buffer.from(`${String(timestamp)}.`, 'utf8'), raw]))\n .digest('hex');\n return `t=${String(timestamp)},v1=${digest}`;\n}\n\nexport interface GpmPayWebhookEvent {\n /** From `X-GPMPay-Event`; defaults to `transaction.created`. */\n type: string;\n /** Unix seconds the delivery was signed at. */\n timestamp: number;\n payload: WebhookPayload;\n /** The raw body, for logging or re-verification. */\n rawBody: string;\n}\n\nfunction headerValue(\n headers: Record<string, string | string[] | undefined> | undefined,\n name: string,\n): string | undefined {\n if (!headers) return undefined;\n const target = name.toLowerCase();\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() !== target) continue;\n return Array.isArray(value) ? value[0] : value;\n }\n return undefined;\n}\n\n/**\n * Verify a delivery and parse it into a typed event.\n *\n * @throws {GpmPayWebhookSignatureError} when verification fails.\n */\nexport function constructWebhookEvent(\n input: VerifyWebhookInput & {\n headers?: Record<string, string | string[] | undefined>;\n },\n): GpmPayWebhookEvent {\n const { timestamp } = assertWebhookSignature(input);\n const rawBody = toBuffer(input.rawBody).toString('utf8');\n\n return {\n type: headerValue(input.headers, EVENT_HEADER) ?? 'transaction.created',\n timestamp,\n payload: JSON.parse(rawBody) as WebhookPayload,\n rawBody,\n };\n}\n\n/**\n * Constant-time comparison for webhooks configured with\n * `authorizationType: 'API_KEY'`, where the raw secret is sent in a header\n * instead of being used to sign the body.\n */\nexport function verifyApiKeyHeader(\n received: string | undefined,\n expected: string,\n): boolean {\n if (typeof received !== 'string' || expected === '') return false;\n const a = Buffer.from(received, 'utf8');\n const b = Buffer.from(expected, 'utf8');\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n","import { GpmPayConfigError, GpmPayWebhookSignatureError } from '../core/errors.js';\nimport {\n SIGNATURE_HEADER,\n constructWebhookEvent,\n type GpmPayWebhookEvent,\n} from './verify.js';\n\n/**\n * Structurally-typed request. Deliberately not `express.Request` — this package\n * has zero dependencies and must not force Express into your tree.\n */\nexport interface MinimalRequest {\n headers: Record<string, string | string[] | undefined>;\n body?: unknown;\n rawBody?: unknown;\n}\n\nexport interface MinimalResponse {\n status(code: number): MinimalResponse;\n json(body: unknown): unknown;\n send(body?: unknown): unknown;\n}\n\nexport type NextFunction = (error?: unknown) => void;\n\nexport interface GpmpayWebhookOptions {\n /** The webhook setting's `authorizationSecret`. */\n secret: string;\n onEvent: (\n event: GpmPayWebhookEvent,\n req: MinimalRequest,\n ) => void | Promise<void>;\n toleranceSeconds?: number;\n /** Defaults to `X-GPMPay-Signature`. */\n headerName?: string;\n onError?: (error: GpmPayWebhookSignatureError, req: MinimalRequest) => void;\n /**\n * Respond 200 before awaiting `onEvent`. Default true.\n *\n * GPM Pay aborts a delivery after 5s and retries on the schedule\n * `[10s, 30s, 2m, 10m, 1h, 6h]`, so a slow handler causes duplicate\n * processing. Acknowledge fast, work afterwards.\n */\n respondEarly?: boolean;\n}\n\nconst BODY_PARSER_HINT =\n 'The GPM Pay webhook handler received an already-parsed body.\\n' +\n 'HMAC verification runs over the exact bytes GPM Pay sent, and re-serializing a\\n' +\n 'parsed object changes them — verification will ALWAYS fail.\\n\\n' +\n 'Mount the raw body parser on this route only:\\n' +\n \" app.post('/webhooks/gpmpay',\\n\" +\n \" express.raw({ type: 'application/json' }),\\n\" +\n ' gpmpayWebhook({ secret, onEvent }));\\n\\n' +\n 'If a global express.json() already ran, capture the raw body with its verify hook:\\n' +\n ' app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));';\n\nfunction resolveRawBody(req: MinimalRequest): Buffer | string {\n if (Buffer.isBuffer(req.rawBody)) return req.rawBody;\n if (typeof req.rawBody === 'string') return req.rawBody;\n if (Buffer.isBuffer(req.body)) return req.body;\n if (typeof req.body === 'string') return req.body;\n\n throw new GpmPayConfigError(BODY_PARSER_HINT);\n}\n\n/**\n * Express-compatible middleware that verifies the signature and hands you a\n * typed event.\n *\n * @example\n * app.post(\n * '/webhooks/gpmpay',\n * express.raw({ type: 'application/json' }),\n * gpmpayWebhook({\n * secret: process.env.GPMPAY_WEBHOOK_SECRET!,\n * onEvent: async (event) => {\n * if (event.payload.transferType !== 'in') return;\n * const code = /DH(\\d+)/.exec(event.payload.content)?.[0];\n * if (code) await fulfil(code, event.payload.transferAmount);\n * },\n * }),\n * );\n */\nexport function gpmpayWebhook(options: GpmpayWebhookOptions) {\n const {\n secret,\n onEvent,\n toleranceSeconds,\n headerName = SIGNATURE_HEADER,\n onError,\n respondEarly = true,\n } = options;\n\n return async function handler(\n req: MinimalRequest,\n res: MinimalResponse,\n next: NextFunction,\n ): Promise<void> {\n let event: GpmPayWebhookEvent;\n\n try {\n const rawBody = resolveRawBody(req);\n const headerKey = Object.keys(req.headers).find(\n (key) => key.toLowerCase() === headerName.toLowerCase(),\n );\n const rawHeader = headerKey === undefined ? undefined : req.headers[headerKey];\n const signature = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;\n\n event = constructWebhookEvent({\n rawBody,\n signature: signature ?? '',\n secret,\n headers: req.headers,\n ...(toleranceSeconds === undefined ? {} : { toleranceSeconds }),\n });\n } catch (error) {\n if (error instanceof GpmPayWebhookSignatureError) {\n onError?.(error, req);\n res.status(401).json({ error: error.message, reason: error.reason });\n return;\n }\n next(error);\n return;\n }\n\n if (respondEarly) {\n res.status(200).json({ received: true });\n try {\n await onEvent(event, req);\n } catch (error) {\n next(error);\n }\n return;\n }\n\n try {\n await onEvent(event, req);\n res.status(200).json({ received: true });\n } catch (error) {\n next(error);\n }\n };\n}\n","import { GpmPayWebhookSignatureError } from '../core/errors.js';\nimport {\n SIGNATURE_HEADER,\n constructWebhookEvent,\n type GpmPayWebhookEvent,\n} from './verify.js';\n\nexport interface NextVerifyOptions {\n secret: string;\n toleranceSeconds?: number;\n /** Defaults to `X-GPMPay-Signature`. */\n headerName?: string;\n}\n\n/**\n * Verify a webhook in a Next.js App Router route handler.\n *\n * `await request.text()` yields the exact bytes GPM Pay sent, so no special\n * body-parser configuration is needed.\n *\n * @throws {GpmPayWebhookSignatureError}\n */\nexport async function verifyNextRequest(\n request: Request,\n options: NextVerifyOptions,\n): Promise<GpmPayWebhookEvent> {\n const { secret, toleranceSeconds, headerName = SIGNATURE_HEADER } = options;\n\n const rawBody = await request.text();\n const headers: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n return constructWebhookEvent({\n rawBody,\n signature: request.headers.get(headerName) ?? '',\n secret,\n headers,\n ...(toleranceSeconds === undefined ? {} : { toleranceSeconds }),\n });\n}\n\n/**\n * A ready-made App Router POST handler.\n *\n * @example\n * // app/api/webhooks/gpmpay/route.ts\n * import { createNextWebhookHandler } from '@gpmpay/sdk/webhooks';\n *\n * export const POST = createNextWebhookHandler({\n * secret: process.env.GPMPAY_WEBHOOK_SECRET!,\n * onEvent: async (event) => {\n * if (event.payload.transferType !== 'in') return;\n * const code = /DH(\\d+)/.exec(event.payload.content)?.[0];\n * if (code) await fulfil(code, event.payload.transferAmount);\n * },\n * });\n */\nexport function createNextWebhookHandler(\n options: NextVerifyOptions & {\n onEvent: (event: GpmPayWebhookEvent) => void | Promise<void>;\n },\n): (request: Request) => Promise<Response> {\n const { onEvent, ...verifyOptions } = options;\n\n return async function POST(request: Request): Promise<Response> {\n let event: GpmPayWebhookEvent;\n try {\n event = await verifyNextRequest(request, verifyOptions);\n } catch (error) {\n if (error instanceof GpmPayWebhookSignatureError) {\n return Response.json(\n { error: error.message, reason: error.reason },\n { status: 401 },\n );\n }\n throw error;\n }\n\n await onEvent(event);\n return Response.json({ received: true });\n };\n}\n\n/**\n * Read a raw body from a stream, for the Pages Router.\n *\n * Requires disabling the built-in parser:\n * ```ts\n * export const config = { api: { bodyParser: false } };\n * ```\n *\n * @param maxBytes Guard against unbounded bodies. Default 1 MiB.\n */\nexport async function readRawBody(\n stream: AsyncIterable<Uint8Array>,\n maxBytes = 1024 * 1024,\n): Promise<Buffer> {\n const chunks: Buffer[] = [];\n let total = 0;\n\n for await (const chunk of stream) {\n const buf = Buffer.from(chunk);\n total += buf.length;\n if (total > maxBytes) {\n throw new Error(\n `Webhook body exceeded ${String(maxBytes)} bytes; refusing to buffer more.`,\n );\n }\n chunks.push(buf);\n }\n\n return Buffer.concat(chunks);\n}\n","/**\n * String-literal unions mirroring the backend's Prisma enums and\n * `apps/backend/src/modules/api-tokens/api-token.types.ts`.\n *\n * These are deliberately vendored rather than imported from `@repo/shared-types`\n * (that package is unpublishable). `src/types/__compat__.ts` asserts they stay\n * in sync at compile time.\n */\n\n/** Scopes an API token can carry. */\nexport type ApiTokenScope =\n | 'transactions:read'\n | 'bank-accounts:read'\n | 'webhooks:manage';\n\nexport const ALL_API_SCOPES: readonly ApiTokenScope[] = [\n 'transactions:read',\n 'bank-accounts:read',\n 'webhooks:manage',\n] as const;\n\n/** `ACTIVE` = usable. `PENDING` = temporarily blocked by the owner. */\nexport type ApiTokenStatus = 'ACTIVE' | 'PENDING';\n\nexport type TransactionType = 'IN' | 'OUT';\n\nexport type TransactionSource = 'REAL' | 'SIMULATED';\n\nexport type BankAccountStatus =\n | 'ACTIVE'\n | 'SUSPENDED'\n | 'DELETED'\n | 'PENDING_VERIFICATION';\n\nexport type WebhookDriver = 'HTTP' | 'TELEGRAM' | 'WORDPRESS' | 'GOOGLE_SHEETS';\n\nexport type WebhookAuthType = 'NONE' | 'API_KEY' | 'HMAC';\n\nexport type WebhookScope = 'ALL' | 'SPECIFIC';\n\nexport type WebhookDeliveryStatus =\n | 'PENDING'\n | 'DELIVERED'\n | 'RETRYING'\n | 'FAILED';\n\n/**\n * Delivery retry backoff used by the GPM Pay webhook dispatcher, in seconds.\n * Mirrors `apps/backend/src/modules/webhook-delivery/backoff.util.ts`.\n *\n * Your endpoint will be called up to {@link WEBHOOK_MAX_ATTEMPTS} times for the\n * same transaction — make it idempotent on `payload.id`.\n */\nexport const WEBHOOK_RETRY_SCHEDULE_SECONDS: readonly number[] = [\n 10, 30, 120, 600, 3600, 21600,\n] as const;\n\nexport const WEBHOOK_MAX_ATTEMPTS = 6;\n\n/**\n * The HTTP delivery driver aborts after this long. Respond within it or the\n * delivery is recorded as failed and retried.\n */\nexport const WEBHOOK_DELIVERY_TIMEOUT_MS = 5000;\n"]}