@open-mercato/shared 0.8.1-develop.7441.1.b42958b98c → 0.8.1-develop.7447.1.ac22e0f0bc
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.
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +7 -3
- package/dist/lib/webhooks/body.js.map +2 -2
- package/dist/lib/webhooks/index.js +2 -0
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +14 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/package.json +2 -2
- package/src/lib/webhooks/__tests__/body.test.ts +48 -0
- package/src/lib/webhooks/body.ts +11 -4
- package/src/lib/webhooks/index.ts +1 -0
- package/src/modules/payment_gateways/__tests__/types.test.ts +44 -0
- package/src/modules/payment_gateways/types.ts +52 -6
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -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.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.8.1-develop.7447.1.ac22e0f0bc';\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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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": "AAAA,SAAS,wCAAwC;
|
|
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": "AAAA,SAAS,wCAAwC;AAiR1C,MAAM,4CAA4C,MAAM;AAAA,EAC7D,YAAY,UAAU,uCAAuC;AAC3D,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAgCA,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,0BAA0B;AAEhC,SAAS,qBAAkD;AACzD,QAAM,cAAc;AAGpB,MAAI,CAAC,YAAY,oBAAoB,GAAG;AACtC,gBAAY,oBAAoB,IAAI,oBAAI,IAA4B;AAAA,EACtE;AACA,SAAO,YAAY,oBAAoB;AACzC;AAEA,SAAS,4BAAqE;AAC5E,QAAM,cAAc;AAGpB,MAAI,CAAC,YAAY,oBAAoB,GAAG;AACtC,gBAAY,oBAAoB,IAAI,oBAAI,IAAwC;AAAA,EAClF;AACA,SAAO,YAAY,oBAAoB;AACzC;AAEA,SAAS,wBAA+D;AACtE,QAAM,cAAc;AAGpB,MAAI,CAAC,YAAY,uBAAuB,GAAG;AACzC,gBAAY,uBAAuB,IAAI,oBAAI,IAAsC;AAAA,EACnF;AACA,SAAO,YAAY,uBAAuB;AAC5C;AAEA,SAAS,WAAW,aAAqB,SAA0B;AACjE,SAAO,UAAU,GAAG,WAAW,IAAI,OAAO,KAAK;AACjD;AAEO,SAAS,uBAAuB,SAAyB,SAA8C;AAC5G,QAAM,kBAAkB,mBAAmB;AAC3C,QAAM,MAAM,WAAW,QAAQ,aAAa,SAAS,OAAO;AAC5D,kBAAgB,IAAI,KAAK,OAAO;AAChC,MAAI,SAAS,SAAS;AAEpB,QAAI,CAAC,gBAAgB,IAAI,QAAQ,WAAW,GAAG;AAC7C,sBAAgB,IAAI,QAAQ,aAAa,OAAO;AAAA,IAClD;AAAA,EACF;AACA,SAAO,MAAM;AACX,oBAAgB,OAAO,GAAG;AAAA,EAC5B;AACF;AAEO,SAAS,kBAAkB,aAAqB,SAA8C;AACnG,QAAM,kBAAkB,mBAAmB;AAC3C,MAAI,SAAS;AACX,WAAO,gBAAgB,IAAI,WAAW,aAAa,OAAO,CAAC,KAAK,gBAAgB,IAAI,WAAW;AAAA,EACjG;AACA,SAAO,gBAAgB,IAAI,WAAW;AACxC;AAEO,SAAS,sBAAwC;AACtD,QAAM,kBAAkB,mBAAmB;AAC3C,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,SAA2B,CAAC;AAClC,aAAW,CAAC,KAAK,OAAO,KAAK,iBAAiB;AAC5C,QAAI,CAAC,IAAI,SAAS,GAAG,KAAK,CAAC,KAAK,IAAI,QAAQ,WAAW,GAAG;AACxD,WAAK,IAAI,QAAQ,WAAW;AAC5B,aAAO,KAAK,OAAO;AAAA,IACrB;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,uBAA6B;AAC3C,qBAAmB,EAAE,MAAM;AAC7B;AAEO,SAAS,uBACd,aACA,SACA,SACY;AACZ,MAAI,SAAS,YAAY,UAAa,QAAQ,YAAY,UAAU,QAAQ,YAAY,SAAS;AAC/F,UAAM,IAAI,MAAM,sEAAsE;AAAA,EACxF;AACA,MACE,SAAS,iBAAiB,WACtB,CAAC,OAAO,cAAc,QAAQ,YAAY,KACzC,QAAQ,gBAAgB,KACxB,QAAQ,eAAe,mCAC5B;AACA,UAAM,IAAI;AAAA,MACR,mGAAmG,gCAAgC;AAAA,IACrI;AAAA,EACF;AACA,QAAM,yBAAyB,0BAA0B;AACzD,yBAAuB,IAAI,aAAa;AAAA,IACtC;AAAA,IACA,OAAO,SAAS;AAAA,IAChB,mBAAmB,SAAS;AAAA,IAC5B,mBAAmB,SAAS;AAAA,IAC5B,cAAc,SAAS;AAAA,IACvB,SAAS,SAAS;AAAA,IAClB,gBAAgB,SAAS;AAAA,EAC3B,CAAC;AACD,SAAO,MAAM;AACX,2BAAuB,OAAO,WAAW;AAAA,EAC3C;AACF;AAEO,SAAS,kBAAkB,aAA6D;AAC7F,SAAO,0BAA0B,EAAE,IAAI,WAAW;AACpD;AAEO,SAAS,uBAA6B;AAC3C,4BAA0B,EAAE,MAAM;AACpC;AAEO,SAAS,iCAAiC,YAAkD;AACjG,QAAM,qBAAqB,sBAAsB;AACjD,qBAAmB,IAAI,WAAW,aAAa,UAAU;AACzD,SAAO,MAAM;AACX,uBAAmB,OAAO,WAAW,WAAW;AAAA,EAClD;AACF;AAEO,SAAS,4BAA4B,aAA2D;AACrG,SAAO,sBAAsB,EAAE,IAAI,WAAW;AAChD;AAEO,SAAS,gCAA4D;AAC1E,SAAO,MAAM,KAAK,sBAAsB,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,MAAM,cAAc,MAAM,KAAK,CAAC;AACjH;AAEO,SAAS,iCAAuC;AACrD,wBAAsB,EAAE,MAAM;AAChC;",
|
|
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.
|
|
3
|
+
"version": "0.8.1-develop.7447.1.ac22e0f0bc",
|
|
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.
|
|
124
|
+
"@open-mercato/cache": "0.8.1-develop.7447.1.ac22e0f0bc",
|
|
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)
|
package/src/lib/webhooks/body.ts
CHANGED
|
@@ -23,10 +23,10 @@ export function resolveWebhookBodyLimitBytes(
|
|
|
23
23
|
return Number.isSafeInteger(parsed) ? parsed : fallback
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export async function
|
|
26
|
+
export async function readBoundedRequestBytes(
|
|
27
27
|
request: Request,
|
|
28
28
|
options?: { maxBytes?: number },
|
|
29
|
-
): Promise<
|
|
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
|
|
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
|
}
|
|
@@ -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)
|