@open-mercato/shared 0.8.1-develop.7442.1.4c1dbf1f0e → 0.8.1-develop.7448.1.94eec0a2d6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.8.1-develop.7442.1.4c1dbf1f0e";
1
+ const APP_VERSION = "0.8.1-develop.7448.1.94eec0a2d6";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.8.1-develop.7442.1.4c1dbf1f0e';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.8.1-develop.7448.1.94eec0a2d6';\nexport const appVersion = APP_VERSION;\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -12,7 +12,7 @@ function resolveWebhookBodyLimitBytes(raw = typeof process === "undefined" ? voi
12
12
  const parsed = parseNumberWithDefault(raw, fallback, { min: 1 });
13
13
  return Number.isSafeInteger(parsed) ? parsed : fallback;
14
14
  }
15
- async function readBoundedRequestBody(request, options) {
15
+ async function readBoundedRequestBytes(request, options) {
16
16
  const configuredLimit = options?.maxBytes ?? resolveWebhookBodyLimitBytes();
17
17
  const maxBytes = Number.isSafeInteger(configuredLimit) && configuredLimit > 0 ? configuredLimit : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES;
18
18
  const declaredLength = request.headers.get("content-length")?.trim();
@@ -22,7 +22,7 @@ async function readBoundedRequestBody(request, options) {
22
22
  throw new WebhookBodyTooLargeError(maxBytes);
23
23
  }
24
24
  }
25
- if (!request.body) return "";
25
+ if (!request.body) return new Uint8Array(0);
26
26
  const reader = request.body.getReader();
27
27
  const chunks = [];
28
28
  let totalBytes = 0;
@@ -46,12 +46,16 @@ async function readBoundedRequestBody(request, options) {
46
46
  body.set(chunk, offset);
47
47
  offset += chunk.byteLength;
48
48
  }
49
- return new TextDecoder().decode(body);
49
+ return body;
50
+ }
51
+ async function readBoundedRequestBody(request, options) {
52
+ return new TextDecoder().decode(await readBoundedRequestBytes(request, options));
50
53
  }
51
54
  export {
52
55
  DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
53
56
  WebhookBodyTooLargeError,
54
57
  readBoundedRequestBody,
58
+ readBoundedRequestBytes,
55
59
  resolveWebhookBodyLimitBytes
56
60
  };
57
61
  //# sourceMappingURL=body.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/webhooks/body.ts"],
4
- "sourcesContent": ["import { parseNumberWithDefault } from '../number'\n\nexport const DEFAULT_WEBHOOK_BODY_LIMIT_BYTES = 1024 * 1024\n\nexport class WebhookBodyTooLargeError extends Error {\n readonly limitBytes: number\n\n constructor(limitBytes: number) {\n super(`Webhook body exceeds the ${limitBytes}-byte limit`)\n this.name = 'WebhookBodyTooLargeError'\n this.limitBytes = limitBytes\n }\n}\n\nexport function resolveWebhookBodyLimitBytes(\n raw = typeof process === 'undefined' ? undefined : process.env.OM_WEBHOOK_MAX_BODY_BYTES,\n fallbackBytes = DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,\n): number {\n const fallback = Number.isSafeInteger(fallbackBytes) && fallbackBytes > 0\n ? fallbackBytes\n : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES\n const parsed = parseNumberWithDefault(raw, fallback, { min: 1 })\n return Number.isSafeInteger(parsed) ? parsed : fallback\n}\n\nexport async function readBoundedRequestBody(\n request: Request,\n options?: { maxBytes?: number },\n): Promise<string> {\n const configuredLimit = options?.maxBytes ?? resolveWebhookBodyLimitBytes()\n const maxBytes = Number.isSafeInteger(configuredLimit) && configuredLimit > 0\n ? configuredLimit\n : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES\n const declaredLength = request.headers.get('content-length')?.trim()\n if (declaredLength && /^\\d+$/.test(declaredLength)) {\n const declaredBytes = Number(declaredLength)\n if (!Number.isSafeInteger(declaredBytes) || declaredBytes > maxBytes) {\n throw new WebhookBodyTooLargeError(maxBytes)\n }\n }\n\n if (!request.body) return ''\n\n const reader = request.body.getReader()\n const chunks: Uint8Array[] = []\n let totalBytes = 0\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n totalBytes += value.byteLength\n if (totalBytes > maxBytes) {\n await reader.cancel().catch(() => undefined)\n throw new WebhookBodyTooLargeError(maxBytes)\n }\n chunks.push(value)\n }\n } finally {\n reader.releaseLock()\n }\n\n const body = new Uint8Array(totalBytes)\n let offset = 0\n for (const chunk of chunks) {\n body.set(chunk, offset)\n offset += chunk.byteLength\n }\n return new TextDecoder().decode(body)\n}\n"],
5
- "mappings": "AAAA,SAAS,8BAA8B;AAEhC,MAAM,mCAAmC,OAAO;AAEhD,MAAM,iCAAiC,MAAM;AAAA,EAGlD,YAAY,YAAoB;AAC9B,UAAM,4BAA4B,UAAU,aAAa;AACzD,SAAK,OAAO;AACZ,SAAK,aAAa;AAAA,EACpB;AACF;AAEO,SAAS,6BACd,MAAM,OAAO,YAAY,cAAc,SAAY,QAAQ,IAAI,2BAC/D,gBAAgB,kCACR;AACR,QAAM,WAAW,OAAO,cAAc,aAAa,KAAK,gBAAgB,IACpE,gBACA;AACJ,QAAM,SAAS,uBAAuB,KAAK,UAAU,EAAE,KAAK,EAAE,CAAC;AAC/D,SAAO,OAAO,cAAc,MAAM,IAAI,SAAS;AACjD;AAEA,eAAsB,uBACpB,SACA,SACiB;AACjB,QAAM,kBAAkB,SAAS,YAAY,6BAA6B;AAC1E,QAAM,WAAW,OAAO,cAAc,eAAe,KAAK,kBAAkB,IACxE,kBACA;AACJ,QAAM,iBAAiB,QAAQ,QAAQ,IAAI,gBAAgB,GAAG,KAAK;AACnE,MAAI,kBAAkB,QAAQ,KAAK,cAAc,GAAG;AAClD,UAAM,gBAAgB,OAAO,cAAc;AAC3C,QAAI,CAAC,OAAO,cAAc,aAAa,KAAK,gBAAgB,UAAU;AACpE,YAAM,IAAI,yBAAyB,QAAQ;AAAA,IAC7C;AAAA,EACF;AAEA,MAAI,CAAC,QAAQ,KAAM,QAAO;AAE1B,QAAM,SAAS,QAAQ,KAAK,UAAU;AACtC,QAAM,SAAuB,CAAC;AAC9B,MAAI,aAAa;AAEjB,MAAI;AACF,WAAO,MAAM;AACX,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AACV,oBAAc,MAAM;AACpB,UAAI,aAAa,UAAU;AACzB,cAAM,OAAO,OAAO,EAAE,MAAM,MAAM,MAAS;AAC3C,cAAM,IAAI,yBAAyB,QAAQ;AAAA,MAC7C;AACA,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AAAA,EACrB;AAEA,QAAM,OAAO,IAAI,WAAW,UAAU;AACtC,MAAI,SAAS;AACb,aAAW,SAAS,QAAQ;AAC1B,SAAK,IAAI,OAAO,MAAM;AACtB,cAAU,MAAM;AAAA,EAClB;AACA,SAAO,IAAI,YAAY,EAAE,OAAO,IAAI;AACtC;",
4
+ "sourcesContent": ["import { parseNumberWithDefault } from '../number'\n\nexport const DEFAULT_WEBHOOK_BODY_LIMIT_BYTES = 1024 * 1024\n\nexport class WebhookBodyTooLargeError extends Error {\n readonly limitBytes: number\n\n constructor(limitBytes: number) {\n super(`Webhook body exceeds the ${limitBytes}-byte limit`)\n this.name = 'WebhookBodyTooLargeError'\n this.limitBytes = limitBytes\n }\n}\n\nexport function resolveWebhookBodyLimitBytes(\n raw = typeof process === 'undefined' ? undefined : process.env.OM_WEBHOOK_MAX_BODY_BYTES,\n fallbackBytes = DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,\n): number {\n const fallback = Number.isSafeInteger(fallbackBytes) && fallbackBytes > 0\n ? fallbackBytes\n : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES\n const parsed = parseNumberWithDefault(raw, fallback, { min: 1 })\n return Number.isSafeInteger(parsed) ? parsed : fallback\n}\n\nexport async function readBoundedRequestBytes(\n request: Request,\n options?: { maxBytes?: number },\n): Promise<Uint8Array> {\n const configuredLimit = options?.maxBytes ?? resolveWebhookBodyLimitBytes()\n const maxBytes = Number.isSafeInteger(configuredLimit) && configuredLimit > 0\n ? configuredLimit\n : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES\n const declaredLength = request.headers.get('content-length')?.trim()\n if (declaredLength && /^\\d+$/.test(declaredLength)) {\n const declaredBytes = Number(declaredLength)\n if (!Number.isSafeInteger(declaredBytes) || declaredBytes > maxBytes) {\n throw new WebhookBodyTooLargeError(maxBytes)\n }\n }\n\n if (!request.body) return new Uint8Array(0)\n\n const reader = request.body.getReader()\n const chunks: Uint8Array[] = []\n let totalBytes = 0\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n totalBytes += value.byteLength\n if (totalBytes > maxBytes) {\n await reader.cancel().catch(() => undefined)\n throw new WebhookBodyTooLargeError(maxBytes)\n }\n chunks.push(value)\n }\n } finally {\n reader.releaseLock()\n }\n\n const body = new Uint8Array(totalBytes)\n let offset = 0\n for (const chunk of chunks) {\n body.set(chunk, offset)\n offset += chunk.byteLength\n }\n return body\n}\n\nexport async function readBoundedRequestBody(\n request: Request,\n options?: { maxBytes?: number },\n): Promise<string> {\n return new TextDecoder().decode(await readBoundedRequestBytes(request, options))\n}\n"],
5
+ "mappings": "AAAA,SAAS,8BAA8B;AAEhC,MAAM,mCAAmC,OAAO;AAEhD,MAAM,iCAAiC,MAAM;AAAA,EAGlD,YAAY,YAAoB;AAC9B,UAAM,4BAA4B,UAAU,aAAa;AACzD,SAAK,OAAO;AACZ,SAAK,aAAa;AAAA,EACpB;AACF;AAEO,SAAS,6BACd,MAAM,OAAO,YAAY,cAAc,SAAY,QAAQ,IAAI,2BAC/D,gBAAgB,kCACR;AACR,QAAM,WAAW,OAAO,cAAc,aAAa,KAAK,gBAAgB,IACpE,gBACA;AACJ,QAAM,SAAS,uBAAuB,KAAK,UAAU,EAAE,KAAK,EAAE,CAAC;AAC/D,SAAO,OAAO,cAAc,MAAM,IAAI,SAAS;AACjD;AAEA,eAAsB,wBACpB,SACA,SACqB;AACrB,QAAM,kBAAkB,SAAS,YAAY,6BAA6B;AAC1E,QAAM,WAAW,OAAO,cAAc,eAAe,KAAK,kBAAkB,IACxE,kBACA;AACJ,QAAM,iBAAiB,QAAQ,QAAQ,IAAI,gBAAgB,GAAG,KAAK;AACnE,MAAI,kBAAkB,QAAQ,KAAK,cAAc,GAAG;AAClD,UAAM,gBAAgB,OAAO,cAAc;AAC3C,QAAI,CAAC,OAAO,cAAc,aAAa,KAAK,gBAAgB,UAAU;AACpE,YAAM,IAAI,yBAAyB,QAAQ;AAAA,IAC7C;AAAA,EACF;AAEA,MAAI,CAAC,QAAQ,KAAM,QAAO,IAAI,WAAW,CAAC;AAE1C,QAAM,SAAS,QAAQ,KAAK,UAAU;AACtC,QAAM,SAAuB,CAAC;AAC9B,MAAI,aAAa;AAEjB,MAAI;AACF,WAAO,MAAM;AACX,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AACV,oBAAc,MAAM;AACpB,UAAI,aAAa,UAAU;AACzB,cAAM,OAAO,OAAO,EAAE,MAAM,MAAM,MAAS;AAC3C,cAAM,IAAI,yBAAyB,QAAQ;AAAA,MAC7C;AACA,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AAAA,EACrB;AAEA,QAAM,OAAO,IAAI,WAAW,UAAU;AACtC,MAAI,SAAS;AACb,aAAW,SAAS,QAAQ;AAC1B,SAAK,IAAI,OAAO,MAAM;AACtB,cAAU,MAAM;AAAA,EAClB;AACA,SAAO;AACT;AAEA,eAAsB,uBACpB,SACA,SACiB;AACjB,SAAO,IAAI,YAAY,EAAE,OAAO,MAAM,wBAAwB,SAAS,OAAO,CAAC;AACjF;",
6
6
  "names": []
7
7
  }
@@ -4,6 +4,7 @@ import { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from
4
4
  import {
5
5
  DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
6
6
  readBoundedRequestBody,
7
+ readBoundedRequestBytes,
7
8
  resolveWebhookBodyLimitBytes,
8
9
  WebhookBodyTooLargeError
9
10
  } from "./body.js";
@@ -18,6 +19,7 @@ export {
18
19
  isWebhookTimestampWithinTolerance,
19
20
  parseWebhookSecret,
20
21
  readBoundedRequestBody,
22
+ readBoundedRequestBytes,
21
23
  resolveWebhookBodyLimitBytes,
22
24
  signWebhookPayload,
23
25
  verifyWebhookSignature
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/webhooks/index.ts"],
4
- "sourcesContent": ["export { signWebhookPayload, buildWebhookHeaders, generateMessageId } from './sign'\nexport { WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, isWebhookTimestampWithinTolerance, verifyWebhookSignature } from './verify'\nexport { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from './secrets'\nexport {\n DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,\n readBoundedRequestBody,\n resolveWebhookBodyLimitBytes,\n WebhookBodyTooLargeError,\n} from './body'\nexport type { StandardWebhookHeaders, WebhookSigningKey, WebhookVerificationResult, StandardWebhookPayload } from './types'\nexport type {\n InboundWebhookRequest,\n WebhookSourceCredentialField,\n WebhookSourceConfig,\n WebhookHandlerMeta,\n WebhookHandlerPayload,\n WebhookHandlerContext,\n WebhookHandler,\n WebhookHandlerRegistryEntry,\n WebhookHandlerResult,\n WebhookIngestionStatus,\n} from './inbound-types'\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB,qBAAqB,yBAAyB;AAC3E,SAAS,qCAAqC,mCAAmC,8BAA8B;AAC/G,SAAS,uBAAuB,oBAAoB,4BAA4B;AAChF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
4
+ "sourcesContent": ["export { signWebhookPayload, buildWebhookHeaders, generateMessageId } from './sign'\nexport { WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, isWebhookTimestampWithinTolerance, verifyWebhookSignature } from './verify'\nexport { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from './secrets'\nexport {\n DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,\n readBoundedRequestBody,\n readBoundedRequestBytes,\n resolveWebhookBodyLimitBytes,\n WebhookBodyTooLargeError,\n} from './body'\nexport type { StandardWebhookHeaders, WebhookSigningKey, WebhookVerificationResult, StandardWebhookPayload } from './types'\nexport type {\n InboundWebhookRequest,\n WebhookSourceCredentialField,\n WebhookSourceConfig,\n WebhookHandlerMeta,\n WebhookHandlerPayload,\n WebhookHandlerContext,\n WebhookHandler,\n WebhookHandlerRegistryEntry,\n WebhookHandlerResult,\n WebhookIngestionStatus,\n} from './inbound-types'\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB,qBAAqB,yBAAyB;AAC3E,SAAS,qCAAqC,mCAAmC,8BAA8B;AAC/G,SAAS,uBAAuB,oBAAoB,4BAA4B;AAChF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,10 @@
1
1
  import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES } from "../../lib/webhooks/body.js";
2
+ class WebhookVerificationUnavailableError extends Error {
3
+ constructor(message = "Webhook verification is unavailable") {
4
+ super(message);
5
+ this.name = "WebhookVerificationUnavailableError";
6
+ }
7
+ }
2
8
  const ADAPTER_REGISTRY_KEY = "__openMercatoPaymentGatewayAdapters__";
3
9
  const WEBHOOK_REGISTRY_KEY = "__openMercatoPaymentGatewayWebhookHandlers__";
4
10
  const DESCRIPTOR_REGISTRY_KEY = "__openMercatoPaymentGatewayDescriptors__";
@@ -62,6 +68,9 @@ function clearGatewayAdapters() {
62
68
  getAdapterRegistry().clear();
63
69
  }
64
70
  function registerWebhookHandler(providerKey, handler, options) {
71
+ if (options?.rawBody !== void 0 && options.rawBody !== "text" && options.rawBody !== "bytes") {
72
+ throw new Error("[internal] Payment gateway webhook rawBody must be 'text' or 'bytes'");
73
+ }
65
74
  if (options?.maxBodyBytes !== void 0 && (!Number.isSafeInteger(options.maxBodyBytes) || options.maxBodyBytes <= 0 || options.maxBodyBytes > DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)) {
66
75
  throw new Error(
67
76
  `[internal] Payment gateway webhook maxBodyBytes must be a positive safe integer no greater than ${DEFAULT_WEBHOOK_BODY_LIMIT_BYTES}`
@@ -72,7 +81,10 @@ function registerWebhookHandler(providerKey, handler, options) {
72
81
  handler,
73
82
  queue: options?.queue,
74
83
  readSessionIdHint: options?.readSessionIdHint,
75
- maxBodyBytes: options?.maxBodyBytes
84
+ readPaymentIdHint: options?.readPaymentIdHint,
85
+ maxBodyBytes: options?.maxBodyBytes,
86
+ rawBody: options?.rawBody,
87
+ formatResponse: options?.formatResponse
76
88
  });
77
89
  return () => {
78
90
  webhookHandlerRegistry.delete(providerKey);
@@ -101,6 +113,7 @@ function clearPaymentGatewayDescriptors() {
101
113
  getDescriptorRegistry().clear();
102
114
  }
103
115
  export {
116
+ WebhookVerificationUnavailableError,
104
117
  clearGatewayAdapters,
105
118
  clearPaymentGatewayDescriptors,
106
119
  clearWebhookHandlers,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/payment_gateways/types.ts"],
4
- "sourcesContent": ["import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES } from '../../lib/webhooks/body'\n\n// \u2500\u2500 Unified Payment Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type UnifiedPaymentStatus =\n | 'pending'\n | 'authorized'\n | 'captured'\n | 'partially_captured'\n | 'refunded'\n | 'partially_refunded'\n | 'cancelled'\n | 'failed'\n | 'expired'\n | 'unknown'\n\n// \u2500\u2500 GatewayAdapter Interface \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface GatewayAdapter {\n readonly providerKey: string\n\n /** Create a payment session / payment intent */\n createSession(input: CreateSessionInput): Promise<CreateSessionResult>\n\n /** Capture an authorized payment */\n capture(input: CaptureInput): Promise<CaptureResult>\n\n /** Refund a captured payment (full or partial) */\n refund(input: RefundInput): Promise<RefundResult>\n\n /** Cancel / void an authorized payment before capture */\n cancel(input: CancelInput): Promise<CancelResult>\n\n /** Get current payment status from provider */\n getStatus(input: GetStatusInput): Promise<GatewayPaymentStatus>\n\n /** Verify and parse an inbound webhook event */\n verifyWebhook(input: VerifyWebhookInput): Promise<WebhookEvent>\n\n /** Map provider status to unified status */\n mapStatus(providerStatus: string, eventType?: string): UnifiedPaymentStatus\n}\n\nexport type PaymentGatewayDescriptorFieldType =\n | 'text'\n | 'number'\n | 'select'\n | 'multiselect'\n | 'boolean'\n | 'textarea'\n | 'secret'\n | 'url'\n\nexport interface PaymentGatewayDescriptorField {\n key: string\n label: string\n type: PaymentGatewayDescriptorFieldType\n description?: string\n required?: boolean\n options?: Array<{ value: string; label: string }>\n}\n\nexport type PaymentGatewayRendererType = 'embedded' | 'redirect'\n\nexport interface PaymentGatewayRendererDescriptor {\n key: string\n label: string\n type: PaymentGatewayRendererType\n description?: string\n supportedPaymentTypes?: '*' | string[]\n settingsFields?: PaymentGatewayDescriptorField[]\n}\n\nexport interface PaymentGatewayPresentationRequest {\n mode?: 'auto' | PaymentGatewayRendererType\n rendererKey?: string\n rendererSettings?: Record<string, unknown>\n}\n\nexport interface PaymentGatewayDescriptor {\n providerKey: string\n label: string\n sessionConfig?: {\n fields?: PaymentGatewayDescriptorField[]\n supportedCurrencies?: '*' | string[]\n supportedPaymentTypes?: Array<{ value: string; label: string }>\n defaultRendererKey?: string\n renderers?: PaymentGatewayRendererDescriptor[]\n /**\n * @deprecated Use `renderers` and `defaultRendererKey`.\n */\n presentation?: 'embedded' | 'redirect' | 'either'\n /**\n * @deprecated Use `renderers`.\n */\n embeddedRenderers?: string[]\n }\n}\n\nexport interface EmbeddedPaymentGatewayClientSession {\n type: 'embedded'\n rendererKey: string\n payload?: Record<string, unknown>\n settings?: Record<string, unknown>\n}\n\nexport interface RedirectPaymentGatewayClientSession {\n type: 'redirect'\n redirectUrl: string\n target?: 'self' | 'top'\n}\n\nexport type PaymentGatewayClientSession =\n | EmbeddedPaymentGatewayClientSession\n | RedirectPaymentGatewayClientSession\n\n// \u2500\u2500 Order Amount Reconciliation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface PaymentGatewayScope {\n organizationId: string\n tenantId: string\n}\n\nexport interface PaymentOrderTotal {\n orderId: string\n currencyCode: string\n amountDue: number\n}\n\n/**\n * Contract a module that owns orders implements so `payment_gateways` can\n * reconcile a caller-supplied session amount against the authoritative amount\n * due, without depending on that module. Implementations MUST scope the lookup\n * by tenant and organization and MUST return `null` for anything outside the\n * caller's scope, so an out-of-scope order is indistinguishable from a missing\n * one. The `sales` module registers the default implementation under the\n * `paymentOrderTotalResolver` DI name.\n */\nexport interface PaymentOrderTotalResolver {\n resolveOrderTotal(orderId: string, scope: PaymentGatewayScope): Promise<PaymentOrderTotal | null>\n}\n\n// \u2500\u2500 Input / Output Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CreateSessionInput {\n orderId?: string\n paymentId: string\n /** Stable operation key for provider-native session creation idempotency. */\n idempotencyKey?: string\n tenantId: string\n organizationId: string\n amount: number\n currencyCode: string\n captureMethod?: 'automatic' | 'manual'\n paymentTypes?: string[]\n description?: string\n successUrl?: string\n cancelUrl?: string\n metadata?: Record<string, unknown>\n credentials: Record<string, unknown>\n lineItems?: SessionLineItem[]\n presentation?: PaymentGatewayPresentationRequest\n}\n\nexport interface SessionLineItem {\n name: string\n quantity: number\n unitAmount: number\n currencyCode: string\n}\n\nexport interface CreateSessionResult {\n sessionId: string\n clientSecret?: string\n redirectUrl?: string\n status: UnifiedPaymentStatus\n providerData?: Record<string, unknown>\n clientSession?: PaymentGatewayClientSession\n}\n\nexport interface CaptureInput {\n sessionId: string\n amount?: number\n credentials: Record<string, unknown>\n metadata?: Record<string, unknown>\n idempotencyKey?: string\n}\n\nexport interface CaptureResult {\n status: UnifiedPaymentStatus\n capturedAmount: number\n providerData?: Record<string, unknown>\n}\n\nexport interface RefundInput {\n sessionId: string\n amount?: number\n reason?: string\n credentials: Record<string, unknown>\n metadata?: Record<string, unknown>\n idempotencyKey?: string\n}\n\nexport interface RefundResult {\n refundId: string\n status: UnifiedPaymentStatus\n refundedAmount: number\n providerData?: Record<string, unknown>\n}\n\nexport interface CancelInput {\n sessionId: string\n reason?: string\n credentials: Record<string, unknown>\n idempotencyKey?: string\n}\n\nexport interface CancelResult {\n status: UnifiedPaymentStatus\n providerData?: Record<string, unknown>\n}\n\nexport interface GetStatusInput {\n sessionId: string\n credentials: Record<string, unknown>\n}\n\nexport interface GatewayPaymentStatus {\n status: UnifiedPaymentStatus\n amount: number\n amountReceived: number\n currencyCode: string\n providerData?: Record<string, unknown>\n}\n\nexport interface VerifyWebhookInput {\n rawBody: string | Buffer\n headers: Record<string, string | string[] | undefined>\n credentials: Record<string, unknown>\n}\n\nexport interface WebhookEvent {\n eventType: string\n eventId: string\n data: Record<string, unknown>\n idempotencyKey: string\n timestamp: Date\n}\n\n// \u2500\u2500 Webhook Handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface WebhookHandlerRegistration {\n handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>\n queue?: string\n readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null\n maxBodyBytes?: number\n}\n\n// \u2500\u2500 Adapter Registry Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface RegisterAdapterOptions {\n version?: string\n}\n\n// \u2500\u2500 Registries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ADAPTER_REGISTRY_KEY = '__openMercatoPaymentGatewayAdapters__'\nconst WEBHOOK_REGISTRY_KEY = '__openMercatoPaymentGatewayWebhookHandlers__'\nconst DESCRIPTOR_REGISTRY_KEY = '__openMercatoPaymentGatewayDescriptors__'\n\nfunction getAdapterRegistry(): Map<string, GatewayAdapter> {\n const globalState = globalThis as typeof globalThis & {\n [ADAPTER_REGISTRY_KEY]?: Map<string, GatewayAdapter>\n }\n if (!globalState[ADAPTER_REGISTRY_KEY]) {\n globalState[ADAPTER_REGISTRY_KEY] = new Map<string, GatewayAdapter>()\n }\n return globalState[ADAPTER_REGISTRY_KEY]\n}\n\nfunction getWebhookHandlerRegistry(): Map<string, WebhookHandlerRegistration> {\n const globalState = globalThis as typeof globalThis & {\n [WEBHOOK_REGISTRY_KEY]?: Map<string, WebhookHandlerRegistration>\n }\n if (!globalState[WEBHOOK_REGISTRY_KEY]) {\n globalState[WEBHOOK_REGISTRY_KEY] = new Map<string, WebhookHandlerRegistration>()\n }\n return globalState[WEBHOOK_REGISTRY_KEY]\n}\n\nfunction getDescriptorRegistry(): Map<string, PaymentGatewayDescriptor> {\n const globalState = globalThis as typeof globalThis & {\n [DESCRIPTOR_REGISTRY_KEY]?: Map<string, PaymentGatewayDescriptor>\n }\n if (!globalState[DESCRIPTOR_REGISTRY_KEY]) {\n globalState[DESCRIPTOR_REGISTRY_KEY] = new Map<string, PaymentGatewayDescriptor>()\n }\n return globalState[DESCRIPTOR_REGISTRY_KEY]\n}\n\nfunction adapterKey(providerKey: string, version?: string): string {\n return version ? `${providerKey}:${version}` : providerKey\n}\n\nexport function registerGatewayAdapter(adapter: GatewayAdapter, options?: RegisterAdapterOptions): () => void {\n const adapterRegistry = getAdapterRegistry()\n const key = adapterKey(adapter.providerKey, options?.version)\n adapterRegistry.set(key, adapter)\n if (options?.version) {\n // Also register as default if no default exists\n if (!adapterRegistry.has(adapter.providerKey)) {\n adapterRegistry.set(adapter.providerKey, adapter)\n }\n }\n return () => {\n adapterRegistry.delete(key)\n }\n}\n\nexport function getGatewayAdapter(providerKey: string, version?: string): GatewayAdapter | undefined {\n const adapterRegistry = getAdapterRegistry()\n if (version) {\n return adapterRegistry.get(adapterKey(providerKey, version)) ?? adapterRegistry.get(providerKey)\n }\n return adapterRegistry.get(providerKey)\n}\n\nexport function listGatewayAdapters(): GatewayAdapter[] {\n const adapterRegistry = getAdapterRegistry()\n const seen = new Set<string>()\n const result: GatewayAdapter[] = []\n for (const [key, adapter] of adapterRegistry) {\n if (!key.includes(':') && !seen.has(adapter.providerKey)) {\n seen.add(adapter.providerKey)\n result.push(adapter)\n }\n }\n return result\n}\n\nexport function clearGatewayAdapters(): void {\n getAdapterRegistry().clear()\n}\n\nexport function registerWebhookHandler(\n providerKey: string,\n handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>,\n options?: {\n queue?: string\n readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null\n maxBodyBytes?: number\n },\n): () => void {\n if (\n options?.maxBodyBytes !== undefined\n && (!Number.isSafeInteger(options.maxBodyBytes)\n || options.maxBodyBytes <= 0\n || options.maxBodyBytes > DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)\n ) {\n throw new Error(\n `[internal] Payment gateway webhook maxBodyBytes must be a positive safe integer no greater than ${DEFAULT_WEBHOOK_BODY_LIMIT_BYTES}`,\n )\n }\n const webhookHandlerRegistry = getWebhookHandlerRegistry()\n webhookHandlerRegistry.set(providerKey, {\n handler,\n queue: options?.queue,\n readSessionIdHint: options?.readSessionIdHint,\n maxBodyBytes: options?.maxBodyBytes,\n })\n return () => {\n webhookHandlerRegistry.delete(providerKey)\n }\n}\n\nexport function getWebhookHandler(providerKey: string): WebhookHandlerRegistration | undefined {\n return getWebhookHandlerRegistry().get(providerKey)\n}\n\nexport function clearWebhookHandlers(): void {\n getWebhookHandlerRegistry().clear()\n}\n\nexport function registerPaymentGatewayDescriptor(descriptor: PaymentGatewayDescriptor): () => void {\n const descriptorRegistry = getDescriptorRegistry()\n descriptorRegistry.set(descriptor.providerKey, descriptor)\n return () => {\n descriptorRegistry.delete(descriptor.providerKey)\n }\n}\n\nexport function getPaymentGatewayDescriptor(providerKey: string): PaymentGatewayDescriptor | undefined {\n return getDescriptorRegistry().get(providerKey)\n}\n\nexport function listPaymentGatewayDescriptors(): PaymentGatewayDescriptor[] {\n return Array.from(getDescriptorRegistry().values()).sort((left, right) => left.label.localeCompare(right.label))\n}\n\nexport function clearPaymentGatewayDescriptors(): void {\n getDescriptorRegistry().clear()\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES } from '../../lib/webhooks/body'\n\n// \u2500\u2500 Unified Payment Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type UnifiedPaymentStatus =\n | 'pending'\n | 'authorized'\n | 'captured'\n | 'partially_captured'\n | 'refunded'\n | 'partially_refunded'\n | 'cancelled'\n | 'failed'\n | 'expired'\n | 'unknown'\n\n// \u2500\u2500 GatewayAdapter Interface \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface GatewayAdapter {\n readonly providerKey: string\n\n /** Create a payment session / payment intent */\n createSession(input: CreateSessionInput): Promise<CreateSessionResult>\n\n /** Capture an authorized payment */\n capture(input: CaptureInput): Promise<CaptureResult>\n\n /** Refund a captured payment (full or partial) */\n refund(input: RefundInput): Promise<RefundResult>\n\n /** Cancel / void an authorized payment before capture */\n cancel(input: CancelInput): Promise<CancelResult>\n\n /** Get current payment status from provider */\n getStatus(input: GetStatusInput): Promise<GatewayPaymentStatus>\n\n /** Verify and parse an inbound webhook event */\n verifyWebhook(input: VerifyWebhookInput): Promise<WebhookEvent>\n\n /** Map provider status to unified status */\n mapStatus(providerStatus: string, eventType?: string): UnifiedPaymentStatus\n}\n\nexport type PaymentGatewayDescriptorFieldType =\n | 'text'\n | 'number'\n | 'select'\n | 'multiselect'\n | 'boolean'\n | 'textarea'\n | 'secret'\n | 'url'\n\nexport interface PaymentGatewayDescriptorField {\n key: string\n label: string\n type: PaymentGatewayDescriptorFieldType\n description?: string\n required?: boolean\n options?: Array<{ value: string; label: string }>\n}\n\nexport type PaymentGatewayRendererType = 'embedded' | 'redirect'\n\nexport interface PaymentGatewayRendererDescriptor {\n key: string\n label: string\n type: PaymentGatewayRendererType\n description?: string\n supportedPaymentTypes?: '*' | string[]\n settingsFields?: PaymentGatewayDescriptorField[]\n}\n\nexport interface PaymentGatewayPresentationRequest {\n mode?: 'auto' | PaymentGatewayRendererType\n rendererKey?: string\n rendererSettings?: Record<string, unknown>\n}\n\nexport interface PaymentGatewayDescriptor {\n providerKey: string\n label: string\n sessionConfig?: {\n fields?: PaymentGatewayDescriptorField[]\n supportedCurrencies?: '*' | string[]\n supportedPaymentTypes?: Array<{ value: string; label: string }>\n defaultRendererKey?: string\n renderers?: PaymentGatewayRendererDescriptor[]\n /**\n * @deprecated Use `renderers` and `defaultRendererKey`.\n */\n presentation?: 'embedded' | 'redirect' | 'either'\n /**\n * @deprecated Use `renderers`.\n */\n embeddedRenderers?: string[]\n }\n}\n\nexport interface EmbeddedPaymentGatewayClientSession {\n type: 'embedded'\n rendererKey: string\n payload?: Record<string, unknown>\n settings?: Record<string, unknown>\n}\n\nexport interface RedirectPaymentGatewayClientSession {\n type: 'redirect'\n redirectUrl: string\n target?: 'self' | 'top'\n}\n\nexport type PaymentGatewayClientSession =\n | EmbeddedPaymentGatewayClientSession\n | RedirectPaymentGatewayClientSession\n\n// \u2500\u2500 Order Amount Reconciliation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface PaymentGatewayScope {\n organizationId: string\n tenantId: string\n}\n\nexport interface PaymentOrderTotal {\n orderId: string\n currencyCode: string\n amountDue: number\n}\n\n/**\n * Contract a module that owns orders implements so `payment_gateways` can\n * reconcile a caller-supplied session amount against the authoritative amount\n * due, without depending on that module. Implementations MUST scope the lookup\n * by tenant and organization and MUST return `null` for anything outside the\n * caller's scope, so an out-of-scope order is indistinguishable from a missing\n * one. The `sales` module registers the default implementation under the\n * `paymentOrderTotalResolver` DI name.\n */\nexport interface PaymentOrderTotalResolver {\n resolveOrderTotal(orderId: string, scope: PaymentGatewayScope): Promise<PaymentOrderTotal | null>\n}\n\n// \u2500\u2500 Input / Output Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CreateSessionInput {\n orderId?: string\n paymentId: string\n /** Stable operation key for provider-native session creation idempotency. */\n idempotencyKey?: string\n tenantId: string\n organizationId: string\n amount: number\n currencyCode: string\n captureMethod?: 'automatic' | 'manual'\n paymentTypes?: string[]\n description?: string\n successUrl?: string\n cancelUrl?: string\n metadata?: Record<string, unknown>\n credentials: Record<string, unknown>\n lineItems?: SessionLineItem[]\n presentation?: PaymentGatewayPresentationRequest\n}\n\nexport interface SessionLineItem {\n name: string\n quantity: number\n unitAmount: number\n currencyCode: string\n}\n\nexport interface CreateSessionResult {\n sessionId: string\n clientSecret?: string\n redirectUrl?: string\n status: UnifiedPaymentStatus\n providerData?: Record<string, unknown>\n clientSession?: PaymentGatewayClientSession\n}\n\nexport interface CaptureInput {\n sessionId: string\n amount?: number\n credentials: Record<string, unknown>\n metadata?: Record<string, unknown>\n idempotencyKey?: string\n}\n\nexport interface CaptureResult {\n status: UnifiedPaymentStatus\n capturedAmount: number\n providerData?: Record<string, unknown>\n}\n\nexport interface RefundInput {\n sessionId: string\n amount?: number\n reason?: string\n credentials: Record<string, unknown>\n metadata?: Record<string, unknown>\n idempotencyKey?: string\n}\n\nexport interface RefundResult {\n refundId: string\n status: UnifiedPaymentStatus\n refundedAmount: number\n providerData?: Record<string, unknown>\n}\n\nexport interface CancelInput {\n sessionId: string\n reason?: string\n credentials: Record<string, unknown>\n idempotencyKey?: string\n}\n\nexport interface CancelResult {\n status: UnifiedPaymentStatus\n providerData?: Record<string, unknown>\n}\n\nexport interface GetStatusInput {\n sessionId: string\n credentials: Record<string, unknown>\n}\n\nexport interface GatewayPaymentStatus {\n status: UnifiedPaymentStatus\n amount: number\n amountReceived: number\n currencyCode: string\n providerData?: Record<string, unknown>\n}\n\nexport interface VerifyWebhookInput {\n rawBody: string | Buffer\n headers: Record<string, string | string[] | undefined>\n credentials: Record<string, unknown>\n /** Platform-resolved payment the webhook is expected to concern, when a locator found one. */\n candidate?: WebhookCandidateSnapshot\n}\n\n/** Raw request material handed to webhook locator hints. */\nexport interface WebhookLocatorContext {\n rawBody: string | Buffer\n headers: Record<string, string | string[] | undefined>\n}\n\nexport interface WebhookCandidateSnapshot {\n transactionId: string\n paymentId: string\n providerSessionId: string | null\n amount: string\n currencyCode: string\n}\n\nexport type WebhookResponseOutcome =\n | 'accepted'\n | 'no_candidate'\n | 'verification_failed'\n | 'verification_unavailable'\n | 'processing_failed'\n | 'payload_too_large'\n | 'rate_limited'\n\nexport interface WebhookHttpResponse {\n status: number\n body: string | Record<string, unknown>\n contentType?: string\n}\n\n/** Thrown by a webhook handler when verification cannot be performed (for example a provider outage). */\nexport class WebhookVerificationUnavailableError extends Error {\n constructor(message = 'Webhook verification is unavailable') {\n super(message)\n this.name = 'WebhookVerificationUnavailableError'\n }\n}\n\nexport interface WebhookEvent {\n eventType: string\n eventId: string\n data: Record<string, unknown>\n idempotencyKey: string\n timestamp: Date\n}\n\n// \u2500\u2500 Webhook Handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface WebhookHandlerRegistration {\n handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>\n queue?: string\n readSessionIdHint?: (payload: Record<string, unknown> | null, context?: WebhookLocatorContext) => string | null\n readPaymentIdHint?: (payload: Record<string, unknown> | null, context?: WebhookLocatorContext) => string | null\n maxBodyBytes?: number\n rawBody?: WebhookRawBodyMode\n formatResponse?: (outcome: WebhookResponseOutcome) => WebhookHttpResponse\n}\n\nexport type WebhookRawBodyMode = 'text' | 'bytes'\n\n// \u2500\u2500 Adapter Registry Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface RegisterAdapterOptions {\n version?: string\n}\n\n// \u2500\u2500 Registries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ADAPTER_REGISTRY_KEY = '__openMercatoPaymentGatewayAdapters__'\nconst WEBHOOK_REGISTRY_KEY = '__openMercatoPaymentGatewayWebhookHandlers__'\nconst DESCRIPTOR_REGISTRY_KEY = '__openMercatoPaymentGatewayDescriptors__'\n\nfunction getAdapterRegistry(): Map<string, GatewayAdapter> {\n const globalState = globalThis as typeof globalThis & {\n [ADAPTER_REGISTRY_KEY]?: Map<string, GatewayAdapter>\n }\n if (!globalState[ADAPTER_REGISTRY_KEY]) {\n globalState[ADAPTER_REGISTRY_KEY] = new Map<string, GatewayAdapter>()\n }\n return globalState[ADAPTER_REGISTRY_KEY]\n}\n\nfunction getWebhookHandlerRegistry(): Map<string, WebhookHandlerRegistration> {\n const globalState = globalThis as typeof globalThis & {\n [WEBHOOK_REGISTRY_KEY]?: Map<string, WebhookHandlerRegistration>\n }\n if (!globalState[WEBHOOK_REGISTRY_KEY]) {\n globalState[WEBHOOK_REGISTRY_KEY] = new Map<string, WebhookHandlerRegistration>()\n }\n return globalState[WEBHOOK_REGISTRY_KEY]\n}\n\nfunction getDescriptorRegistry(): Map<string, PaymentGatewayDescriptor> {\n const globalState = globalThis as typeof globalThis & {\n [DESCRIPTOR_REGISTRY_KEY]?: Map<string, PaymentGatewayDescriptor>\n }\n if (!globalState[DESCRIPTOR_REGISTRY_KEY]) {\n globalState[DESCRIPTOR_REGISTRY_KEY] = new Map<string, PaymentGatewayDescriptor>()\n }\n return globalState[DESCRIPTOR_REGISTRY_KEY]\n}\n\nfunction adapterKey(providerKey: string, version?: string): string {\n return version ? `${providerKey}:${version}` : providerKey\n}\n\nexport function registerGatewayAdapter(adapter: GatewayAdapter, options?: RegisterAdapterOptions): () => void {\n const adapterRegistry = getAdapterRegistry()\n const key = adapterKey(adapter.providerKey, options?.version)\n adapterRegistry.set(key, adapter)\n if (options?.version) {\n // Also register as default if no default exists\n if (!adapterRegistry.has(adapter.providerKey)) {\n adapterRegistry.set(adapter.providerKey, adapter)\n }\n }\n return () => {\n adapterRegistry.delete(key)\n }\n}\n\nexport function getGatewayAdapter(providerKey: string, version?: string): GatewayAdapter | undefined {\n const adapterRegistry = getAdapterRegistry()\n if (version) {\n return adapterRegistry.get(adapterKey(providerKey, version)) ?? adapterRegistry.get(providerKey)\n }\n return adapterRegistry.get(providerKey)\n}\n\nexport function listGatewayAdapters(): GatewayAdapter[] {\n const adapterRegistry = getAdapterRegistry()\n const seen = new Set<string>()\n const result: GatewayAdapter[] = []\n for (const [key, adapter] of adapterRegistry) {\n if (!key.includes(':') && !seen.has(adapter.providerKey)) {\n seen.add(adapter.providerKey)\n result.push(adapter)\n }\n }\n return result\n}\n\nexport function clearGatewayAdapters(): void {\n getAdapterRegistry().clear()\n}\n\nexport function registerWebhookHandler(\n providerKey: string,\n handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>,\n options?: Omit<WebhookHandlerRegistration, 'handler'>,\n): () => void {\n if (options?.rawBody !== undefined && options.rawBody !== 'text' && options.rawBody !== 'bytes') {\n throw new Error(\"[internal] Payment gateway webhook rawBody must be 'text' or 'bytes'\")\n }\n if (\n options?.maxBodyBytes !== undefined\n && (!Number.isSafeInteger(options.maxBodyBytes)\n || options.maxBodyBytes <= 0\n || options.maxBodyBytes > DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)\n ) {\n throw new Error(\n `[internal] Payment gateway webhook maxBodyBytes must be a positive safe integer no greater than ${DEFAULT_WEBHOOK_BODY_LIMIT_BYTES}`,\n )\n }\n const webhookHandlerRegistry = getWebhookHandlerRegistry()\n webhookHandlerRegistry.set(providerKey, {\n handler,\n queue: options?.queue,\n readSessionIdHint: options?.readSessionIdHint,\n readPaymentIdHint: options?.readPaymentIdHint,\n maxBodyBytes: options?.maxBodyBytes,\n rawBody: options?.rawBody,\n formatResponse: options?.formatResponse,\n })\n return () => {\n webhookHandlerRegistry.delete(providerKey)\n }\n}\n\nexport function getWebhookHandler(providerKey: string): WebhookHandlerRegistration | undefined {\n return getWebhookHandlerRegistry().get(providerKey)\n}\n\nexport function clearWebhookHandlers(): void {\n getWebhookHandlerRegistry().clear()\n}\n\nexport function registerPaymentGatewayDescriptor(descriptor: PaymentGatewayDescriptor): () => void {\n const descriptorRegistry = getDescriptorRegistry()\n descriptorRegistry.set(descriptor.providerKey, descriptor)\n return () => {\n descriptorRegistry.delete(descriptor.providerKey)\n }\n}\n\nexport function getPaymentGatewayDescriptor(providerKey: string): PaymentGatewayDescriptor | undefined {\n return getDescriptorRegistry().get(providerKey)\n}\n\nexport function listPaymentGatewayDescriptors(): PaymentGatewayDescriptor[] {\n return Array.from(getDescriptorRegistry().values()).sort((left, right) => left.label.localeCompare(right.label))\n}\n\nexport function clearPaymentGatewayDescriptors(): void {\n getDescriptorRegistry().clear()\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.8.1-develop.7442.1.4c1dbf1f0e",
3
+ "version": "0.8.1-develop.7448.1.94eec0a2d6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -121,7 +121,7 @@
121
121
  "@mikro-orm/core": "^7.1.14",
122
122
  "@mikro-orm/decorators": "^7.1.14",
123
123
  "@mikro-orm/postgresql": "^7.1.14",
124
- "@open-mercato/cache": "0.8.1-develop.7442.1.4c1dbf1f0e",
124
+ "@open-mercato/cache": "0.8.1-develop.7448.1.94eec0a2d6",
125
125
  "@types/html-to-text": "^9.0.4",
126
126
  "@types/sanitize-html": "^2.16.1",
127
127
  "dotenv": "^17.4.2",
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
4
4
  readBoundedRequestBody,
5
+ readBoundedRequestBytes,
5
6
  resolveWebhookBodyLimitBytes,
6
7
  WebhookBodyTooLargeError,
7
8
  } from '../body'
@@ -77,6 +78,53 @@ describe('readBoundedRequestBody', () => {
77
78
  })
78
79
  })
79
80
 
81
+ describe('readBoundedRequestBytes', () => {
82
+ it('preserves exact bytes including invalid UTF-8', async () => {
83
+ const bytes = new Uint8Array([0xff, 0xfe, 0x41, 0xc3, 0x28])
84
+ const { request } = makeStreamingRequest([bytes.slice(0, 2), bytes.slice(2)])
85
+
86
+ const result = await readBoundedRequestBytes(request, { maxBytes: bytes.byteLength })
87
+
88
+ expect(Array.from(result)).toEqual(Array.from(bytes))
89
+ expect(new TextDecoder().decode(result)).toContain('\uFFFD')
90
+ })
91
+
92
+ it('returns an empty array for a missing body', async () => {
93
+ const request = new Request('http://localhost/webhook', { method: 'POST' })
94
+
95
+ expect((await readBoundedRequestBytes(request)).byteLength).toBe(0)
96
+ await expect(readBoundedRequestBody(request)).resolves.toBe('')
97
+ })
98
+
99
+ it('enforces the declared and streamed limits and cancels the stream', async () => {
100
+ const declared = new Request('http://localhost/webhook', {
101
+ method: 'POST',
102
+ headers: { 'content-length': '6' },
103
+ body: 'ok',
104
+ })
105
+ await expect(readBoundedRequestBytes(declared, { maxBytes: 5 })).rejects.toBeInstanceOf(
106
+ WebhookBodyTooLargeError,
107
+ )
108
+
109
+ const { request, cancel } = makeStreamingRequest([
110
+ new Uint8Array([1, 2, 3]),
111
+ new Uint8Array([4, 5, 6]),
112
+ new Uint8Array([7, 8, 9]),
113
+ ])
114
+ await expect(readBoundedRequestBytes(request, { maxBytes: 5 })).rejects.toBeInstanceOf(
115
+ WebhookBodyTooLargeError,
116
+ )
117
+ expect(cancel).toHaveBeenCalled()
118
+ })
119
+
120
+ it('keeps the string reader decoding the same bytes', async () => {
121
+ const bytes = new TextEncoder().encode('zażółć')
122
+ const { request } = makeStreamingRequest([bytes])
123
+
124
+ await expect(readBoundedRequestBody(request)).resolves.toBe('zażółć')
125
+ })
126
+ })
127
+
80
128
  describe('resolveWebhookBodyLimitBytes', () => {
81
129
  it('uses the documented default for missing or invalid configuration', () => {
82
130
  expect(resolveWebhookBodyLimitBytes(undefined)).toBe(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)
@@ -23,10 +23,10 @@ export function resolveWebhookBodyLimitBytes(
23
23
  return Number.isSafeInteger(parsed) ? parsed : fallback
24
24
  }
25
25
 
26
- export async function readBoundedRequestBody(
26
+ export async function readBoundedRequestBytes(
27
27
  request: Request,
28
28
  options?: { maxBytes?: number },
29
- ): Promise<string> {
29
+ ): Promise<Uint8Array> {
30
30
  const configuredLimit = options?.maxBytes ?? resolveWebhookBodyLimitBytes()
31
31
  const maxBytes = Number.isSafeInteger(configuredLimit) && configuredLimit > 0
32
32
  ? configuredLimit
@@ -39,7 +39,7 @@ export async function readBoundedRequestBody(
39
39
  }
40
40
  }
41
41
 
42
- if (!request.body) return ''
42
+ if (!request.body) return new Uint8Array(0)
43
43
 
44
44
  const reader = request.body.getReader()
45
45
  const chunks: Uint8Array[] = []
@@ -66,5 +66,12 @@ export async function readBoundedRequestBody(
66
66
  body.set(chunk, offset)
67
67
  offset += chunk.byteLength
68
68
  }
69
- return new TextDecoder().decode(body)
69
+ return body
70
+ }
71
+
72
+ export async function readBoundedRequestBody(
73
+ request: Request,
74
+ options?: { maxBytes?: number },
75
+ ): Promise<string> {
76
+ return new TextDecoder().decode(await readBoundedRequestBytes(request, options))
70
77
  }
@@ -4,6 +4,7 @@ export { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from
4
4
  export {
5
5
  DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
6
6
  readBoundedRequestBody,
7
+ readBoundedRequestBytes,
7
8
  resolveWebhookBodyLimitBytes,
8
9
  WebhookBodyTooLargeError,
9
10
  } from './body'
@@ -3,6 +3,9 @@ import {
3
3
  getWebhookHandler,
4
4
  registerWebhookHandler,
5
5
  type VerifyWebhookInput,
6
+ type WebhookLocatorContext,
7
+ type WebhookResponseOutcome,
8
+ WebhookVerificationUnavailableError,
6
9
  type WebhookEvent,
7
10
  } from '../types'
8
11
 
@@ -34,4 +37,45 @@ describe('payment gateway webhook registration', () => {
34
37
  expect(getWebhookHandler('invalid')).toBeUndefined()
35
38
  },
36
39
  )
40
+
41
+ it('stays compatible with one-argument locator hints', () => {
42
+ const readSessionIdHint = (payload: Record<string, unknown> | null) => (payload ? 'sess' : null)
43
+ registerWebhookHandler('legacy', handler, { readSessionIdHint })
44
+
45
+ expect(getWebhookHandler('legacy')?.readSessionIdHint?.({})).toBe('sess')
46
+ expect(getWebhookHandler('legacy')?.rawBody).toBeUndefined()
47
+ expect(getWebhookHandler('legacy')?.formatResponse).toBeUndefined()
48
+ })
49
+
50
+ it('passes through the optional extension hooks', () => {
51
+ const readSessionIdHint = (_payload: Record<string, unknown> | null, context?: WebhookLocatorContext) =>
52
+ context ? String(context.headers['x-id']) : null
53
+ const readPaymentIdHint = () => 'pay'
54
+ const formatResponse = (outcome: WebhookResponseOutcome) => ({ status: 200, body: outcome })
55
+ registerWebhookHandler('extended', handler, {
56
+ readSessionIdHint,
57
+ readPaymentIdHint,
58
+ rawBody: 'bytes',
59
+ formatResponse,
60
+ })
61
+
62
+ const registration = getWebhookHandler('extended')
63
+ expect(registration?.rawBody).toBe('bytes')
64
+ expect(registration?.readPaymentIdHint).toBe(readPaymentIdHint)
65
+ expect(registration?.formatResponse).toBe(formatResponse)
66
+ expect(registration?.readSessionIdHint?.(null, { rawBody: '', headers: { 'x-id': 'abc' } })).toBe('abc')
67
+ })
68
+
69
+ it('rejects an unknown rawBody mode', () => {
70
+ expect(() =>
71
+ registerWebhookHandler('bad', handler, { rawBody: 'stream' as unknown as 'text' }),
72
+ ).toThrow('[internal] Payment gateway webhook rawBody must be')
73
+ expect(getWebhookHandler('bad')).toBeUndefined()
74
+ })
75
+
76
+ it('exposes a typed verification-unavailable error', () => {
77
+ const error = new WebhookVerificationUnavailableError()
78
+ expect(error).toBeInstanceOf(Error)
79
+ expect(error.name).toBe('WebhookVerificationUnavailableError')
80
+ })
37
81
  })
@@ -237,6 +237,45 @@ export interface VerifyWebhookInput {
237
237
  rawBody: string | Buffer
238
238
  headers: Record<string, string | string[] | undefined>
239
239
  credentials: Record<string, unknown>
240
+ /** Platform-resolved payment the webhook is expected to concern, when a locator found one. */
241
+ candidate?: WebhookCandidateSnapshot
242
+ }
243
+
244
+ /** Raw request material handed to webhook locator hints. */
245
+ export interface WebhookLocatorContext {
246
+ rawBody: string | Buffer
247
+ headers: Record<string, string | string[] | undefined>
248
+ }
249
+
250
+ export interface WebhookCandidateSnapshot {
251
+ transactionId: string
252
+ paymentId: string
253
+ providerSessionId: string | null
254
+ amount: string
255
+ currencyCode: string
256
+ }
257
+
258
+ export type WebhookResponseOutcome =
259
+ | 'accepted'
260
+ | 'no_candidate'
261
+ | 'verification_failed'
262
+ | 'verification_unavailable'
263
+ | 'processing_failed'
264
+ | 'payload_too_large'
265
+ | 'rate_limited'
266
+
267
+ export interface WebhookHttpResponse {
268
+ status: number
269
+ body: string | Record<string, unknown>
270
+ contentType?: string
271
+ }
272
+
273
+ /** Thrown by a webhook handler when verification cannot be performed (for example a provider outage). */
274
+ export class WebhookVerificationUnavailableError extends Error {
275
+ constructor(message = 'Webhook verification is unavailable') {
276
+ super(message)
277
+ this.name = 'WebhookVerificationUnavailableError'
278
+ }
240
279
  }
241
280
 
242
281
  export interface WebhookEvent {
@@ -252,10 +291,15 @@ export interface WebhookEvent {
252
291
  export interface WebhookHandlerRegistration {
253
292
  handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>
254
293
  queue?: string
255
- readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null
294
+ readSessionIdHint?: (payload: Record<string, unknown> | null, context?: WebhookLocatorContext) => string | null
295
+ readPaymentIdHint?: (payload: Record<string, unknown> | null, context?: WebhookLocatorContext) => string | null
256
296
  maxBodyBytes?: number
297
+ rawBody?: WebhookRawBodyMode
298
+ formatResponse?: (outcome: WebhookResponseOutcome) => WebhookHttpResponse
257
299
  }
258
300
 
301
+ export type WebhookRawBodyMode = 'text' | 'bytes'
302
+
259
303
  // ── Adapter Registry Options ────────────────────────────────────────────────
260
304
 
261
305
  export interface RegisterAdapterOptions {
@@ -345,12 +389,11 @@ export function clearGatewayAdapters(): void {
345
389
  export function registerWebhookHandler(
346
390
  providerKey: string,
347
391
  handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>,
348
- options?: {
349
- queue?: string
350
- readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null
351
- maxBodyBytes?: number
352
- },
392
+ options?: Omit<WebhookHandlerRegistration, 'handler'>,
353
393
  ): () => void {
394
+ if (options?.rawBody !== undefined && options.rawBody !== 'text' && options.rawBody !== 'bytes') {
395
+ throw new Error("[internal] Payment gateway webhook rawBody must be 'text' or 'bytes'")
396
+ }
354
397
  if (
355
398
  options?.maxBodyBytes !== undefined
356
399
  && (!Number.isSafeInteger(options.maxBodyBytes)
@@ -366,7 +409,10 @@ export function registerWebhookHandler(
366
409
  handler,
367
410
  queue: options?.queue,
368
411
  readSessionIdHint: options?.readSessionIdHint,
412
+ readPaymentIdHint: options?.readPaymentIdHint,
369
413
  maxBodyBytes: options?.maxBodyBytes,
414
+ rawBody: options?.rawBody,
415
+ formatResponse: options?.formatResponse,
370
416
  })
371
417
  return () => {
372
418
  webhookHandlerRegistry.delete(providerKey)