@open-mercato/shared 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6814.1.0627c7e9f1

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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/lib/di/container.js +33 -3
  3. package/dist/lib/di/container.js.map +2 -2
  4. package/dist/lib/email/send.js +56 -0
  5. package/dist/lib/email/send.js.map +2 -2
  6. package/dist/lib/query/types.js.map +1 -1
  7. package/dist/lib/search/config.js +24 -2
  8. package/dist/lib/search/config.js.map +2 -2
  9. package/dist/lib/search/tokenize.js +23 -11
  10. package/dist/lib/search/tokenize.js.map +2 -2
  11. package/dist/lib/version.js +1 -1
  12. package/dist/lib/version.js.map +1 -1
  13. package/dist/lib/webhooks/body.js +57 -0
  14. package/dist/lib/webhooks/body.js.map +7 -0
  15. package/dist/lib/webhooks/index.js +13 -1
  16. package/dist/lib/webhooks/index.js.map +2 -2
  17. package/dist/lib/webhooks/verify.js +12 -7
  18. package/dist/lib/webhooks/verify.js.map +2 -2
  19. package/dist/modules/payment_gateways/types.js +8 -1
  20. package/dist/modules/payment_gateways/types.js.map +2 -2
  21. package/dist/modules/widgets/extension-points.js.map +1 -1
  22. package/package.json +2 -2
  23. package/src/lib/di/__tests__/container-app-di-absent.test.ts +139 -0
  24. package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
  25. package/src/lib/di/container.ts +45 -4
  26. package/src/lib/email/__tests__/send.test.ts +37 -1
  27. package/src/lib/email/send.ts +79 -0
  28. package/src/lib/query/types.ts +4 -5
  29. package/src/lib/search/__tests__/config.test.ts +59 -0
  30. package/src/lib/search/__tests__/tokenize.test.ts +49 -0
  31. package/src/lib/search/config.ts +28 -0
  32. package/src/lib/search/tokenize.ts +33 -11
  33. package/src/lib/webhooks/__tests__/body.test.ts +95 -0
  34. package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
  35. package/src/lib/webhooks/body.ts +70 -0
  36. package/src/lib/webhooks/index.ts +7 -1
  37. package/src/lib/webhooks/verify.ts +17 -9
  38. package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
  39. package/src/modules/payment_gateways/types.ts +15 -0
  40. package/src/modules/widgets/extension-points.ts +104 -1
@@ -1,13 +1,16 @@
1
1
  import { timingSafeEqual, createHmac } from "node:crypto";
2
2
  import { parseWebhookSecret } from "./secrets.js";
3
- const TOLERANCE_SECONDS = 5 * 60;
4
- function verifyWebhookSignature(msgId, timestamp, body, signatureHeader, secrets, toleranceSeconds = TOLERANCE_SECONDS) {
5
- const timestampNum = parseInt(timestamp, 10);
6
- if (isNaN(timestampNum)) {
7
- return { valid: false };
3
+ const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
4
+ function isWebhookTimestampWithinTolerance(timestamp, toleranceSeconds = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, nowSeconds = Math.floor(Date.now() / 1e3)) {
5
+ const normalizedTimestamp = timestamp.trim();
6
+ if (!/^\d+$/.test(normalizedTimestamp)) {
7
+ return false;
8
8
  }
9
- const now = Math.floor(Date.now() / 1e3);
10
- if (Math.abs(now - timestampNum) > toleranceSeconds) {
9
+ const timestampNum = Number.parseInt(normalizedTimestamp, 10);
10
+ return Math.abs(nowSeconds - timestampNum) <= toleranceSeconds;
11
+ }
12
+ function verifyWebhookSignature(msgId, timestamp, body, signatureHeader, secrets, toleranceSeconds = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS) {
13
+ if (!isWebhookTimestampWithinTolerance(timestamp, toleranceSeconds)) {
11
14
  return { valid: false };
12
15
  }
13
16
  const signedContent = `${msgId}.${timestamp}.${body}`;
@@ -29,6 +32,8 @@ function verifyWebhookSignature(msgId, timestamp, body, signatureHeader, secrets
29
32
  return { valid: false };
30
33
  }
31
34
  export {
35
+ WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
36
+ isWebhookTimestampWithinTolerance,
32
37
  verifyWebhookSignature
33
38
  };
34
39
  //# sourceMappingURL=verify.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/webhooks/verify.ts"],
4
- "sourcesContent": ["import { timingSafeEqual, createHmac } from 'node:crypto'\nimport { parseWebhookSecret } from './secrets'\nimport type { WebhookVerificationResult } from './types'\n\nconst TOLERANCE_SECONDS = 5 * 60 // 5 minutes\n\n/**\n * Verify a Standard Webhooks signature against one or more secrets.\n * Returns verification result with which key matched.\n */\nexport function verifyWebhookSignature(\n msgId: string,\n timestamp: string,\n body: string,\n signatureHeader: string,\n secrets: string[],\n toleranceSeconds: number = TOLERANCE_SECONDS,\n): WebhookVerificationResult {\n const timestampNum = parseInt(timestamp, 10)\n if (isNaN(timestampNum)) {\n return { valid: false }\n }\n\n const now = Math.floor(Date.now() / 1000)\n if (Math.abs(now - timestampNum) > toleranceSeconds) {\n return { valid: false }\n }\n\n const signedContent = `${msgId}.${timestamp}.${body}`\n const providedSignatures = signatureHeader.split(' ')\n\n for (let keyIndex = 0; keyIndex < secrets.length; keyIndex++) {\n const key = parseWebhookSecret(secrets[keyIndex])\n const expectedSig = createHmac('sha256', key)\n .update(signedContent)\n .digest('base64')\n\n for (const sig of providedSignatures) {\n const parts = sig.split(',')\n if (parts.length !== 2 || parts[0] !== 'v1') continue\n\n const sigBytes = Buffer.from(parts[1], 'base64')\n const expectedBytes = Buffer.from(expectedSig, 'base64')\n\n if (sigBytes.length !== expectedBytes.length) continue\n\n if (timingSafeEqual(sigBytes, expectedBytes)) {\n return { valid: true, matchedKeyIndex: keyIndex }\n }\n }\n }\n\n return { valid: false }\n}\n"],
5
- "mappings": "AAAA,SAAS,iBAAiB,kBAAkB;AAC5C,SAAS,0BAA0B;AAGnC,MAAM,oBAAoB,IAAI;AAMvB,SAAS,uBACd,OACA,WACA,MACA,iBACA,SACA,mBAA2B,mBACA;AAC3B,QAAM,eAAe,SAAS,WAAW,EAAE;AAC3C,MAAI,MAAM,YAAY,GAAG;AACvB,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAEA,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,MAAI,KAAK,IAAI,MAAM,YAAY,IAAI,kBAAkB;AACnD,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAEA,QAAM,gBAAgB,GAAG,KAAK,IAAI,SAAS,IAAI,IAAI;AACnD,QAAM,qBAAqB,gBAAgB,MAAM,GAAG;AAEpD,WAAS,WAAW,GAAG,WAAW,QAAQ,QAAQ,YAAY;AAC5D,UAAM,MAAM,mBAAmB,QAAQ,QAAQ,CAAC;AAChD,UAAM,cAAc,WAAW,UAAU,GAAG,EACzC,OAAO,aAAa,EACpB,OAAO,QAAQ;AAElB,eAAW,OAAO,oBAAoB;AACpC,YAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,UAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,KAAM;AAE7C,YAAM,WAAW,OAAO,KAAK,MAAM,CAAC,GAAG,QAAQ;AAC/C,YAAM,gBAAgB,OAAO,KAAK,aAAa,QAAQ;AAEvD,UAAI,SAAS,WAAW,cAAc,OAAQ;AAE9C,UAAI,gBAAgB,UAAU,aAAa,GAAG;AAC5C,eAAO,EAAE,OAAO,MAAM,iBAAiB,SAAS;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,MAAM;AACxB;",
4
+ "sourcesContent": ["import { timingSafeEqual, createHmac } from 'node:crypto'\nimport { parseWebhookSecret } from './secrets'\nimport type { WebhookVerificationResult } from './types'\n\nexport const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 5 * 60 // 5 minutes\n\nexport function isWebhookTimestampWithinTolerance(\n timestamp: string,\n toleranceSeconds: number = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,\n nowSeconds: number = Math.floor(Date.now() / 1000),\n): boolean {\n const normalizedTimestamp = timestamp.trim()\n if (!/^\\d+$/.test(normalizedTimestamp)) {\n return false\n }\n\n const timestampNum = Number.parseInt(normalizedTimestamp, 10)\n return Math.abs(nowSeconds - timestampNum) <= toleranceSeconds\n}\n\n/**\n * Verify a Standard Webhooks signature against one or more secrets.\n * Returns verification result with which key matched.\n */\nexport function verifyWebhookSignature(\n msgId: string,\n timestamp: string,\n body: string,\n signatureHeader: string,\n secrets: string[],\n toleranceSeconds: number = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,\n): WebhookVerificationResult {\n if (!isWebhookTimestampWithinTolerance(timestamp, toleranceSeconds)) {\n return { valid: false }\n }\n\n const signedContent = `${msgId}.${timestamp}.${body}`\n const providedSignatures = signatureHeader.split(' ')\n\n for (let keyIndex = 0; keyIndex < secrets.length; keyIndex++) {\n const key = parseWebhookSecret(secrets[keyIndex])\n const expectedSig = createHmac('sha256', key)\n .update(signedContent)\n .digest('base64')\n\n for (const sig of providedSignatures) {\n const parts = sig.split(',')\n if (parts.length !== 2 || parts[0] !== 'v1') continue\n\n const sigBytes = Buffer.from(parts[1], 'base64')\n const expectedBytes = Buffer.from(expectedSig, 'base64')\n\n if (sigBytes.length !== expectedBytes.length) continue\n\n if (timingSafeEqual(sigBytes, expectedBytes)) {\n return { valid: true, matchedKeyIndex: keyIndex }\n }\n }\n }\n\n return { valid: false }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB,kBAAkB;AAC5C,SAAS,0BAA0B;AAG5B,MAAM,sCAAsC,IAAI;AAEhD,SAAS,kCACd,WACA,mBAA2B,qCAC3B,aAAqB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GACxC;AACT,QAAM,sBAAsB,UAAU,KAAK;AAC3C,MAAI,CAAC,QAAQ,KAAK,mBAAmB,GAAG;AACtC,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,OAAO,SAAS,qBAAqB,EAAE;AAC5D,SAAO,KAAK,IAAI,aAAa,YAAY,KAAK;AAChD;AAMO,SAAS,uBACd,OACA,WACA,MACA,iBACA,SACA,mBAA2B,qCACA;AAC3B,MAAI,CAAC,kCAAkC,WAAW,gBAAgB,GAAG;AACnE,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAEA,QAAM,gBAAgB,GAAG,KAAK,IAAI,SAAS,IAAI,IAAI;AACnD,QAAM,qBAAqB,gBAAgB,MAAM,GAAG;AAEpD,WAAS,WAAW,GAAG,WAAW,QAAQ,QAAQ,YAAY;AAC5D,UAAM,MAAM,mBAAmB,QAAQ,QAAQ,CAAC;AAChD,UAAM,cAAc,WAAW,UAAU,GAAG,EACzC,OAAO,aAAa,EACpB,OAAO,QAAQ;AAElB,eAAW,OAAO,oBAAoB;AACpC,YAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,UAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,KAAM;AAE7C,YAAM,WAAW,OAAO,KAAK,MAAM,CAAC,GAAG,QAAQ;AAC/C,YAAM,gBAAgB,OAAO,KAAK,aAAa,QAAQ;AAEvD,UAAI,SAAS,WAAW,cAAc,OAAQ;AAE9C,UAAI,gBAAgB,UAAU,aAAa,GAAG;AAC5C,eAAO,EAAE,OAAO,MAAM,iBAAiB,SAAS;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,MAAM;AACxB;",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES } from "../../lib/webhooks/body.js";
1
2
  const ADAPTER_REGISTRY_KEY = "__openMercatoPaymentGatewayAdapters__";
2
3
  const WEBHOOK_REGISTRY_KEY = "__openMercatoPaymentGatewayWebhookHandlers__";
3
4
  const DESCRIPTOR_REGISTRY_KEY = "__openMercatoPaymentGatewayDescriptors__";
@@ -61,11 +62,17 @@ function clearGatewayAdapters() {
61
62
  getAdapterRegistry().clear();
62
63
  }
63
64
  function registerWebhookHandler(providerKey, handler, options) {
65
+ if (options?.maxBodyBytes !== void 0 && (!Number.isSafeInteger(options.maxBodyBytes) || options.maxBodyBytes <= 0 || options.maxBodyBytes > DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)) {
66
+ throw new Error(
67
+ `[internal] Payment gateway webhook maxBodyBytes must be a positive safe integer no greater than ${DEFAULT_WEBHOOK_BODY_LIMIT_BYTES}`
68
+ );
69
+ }
64
70
  const webhookHandlerRegistry = getWebhookHandlerRegistry();
65
71
  webhookHandlerRegistry.set(providerKey, {
66
72
  handler,
67
73
  queue: options?.queue,
68
- readSessionIdHint: options?.readSessionIdHint
74
+ readSessionIdHint: options?.readSessionIdHint,
75
+ maxBodyBytes: options?.maxBodyBytes
69
76
  });
70
77
  return () => {
71
78
  webhookHandlerRegistry.delete(providerKey);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/payment_gateways/types.ts"],
4
- "sourcesContent": ["// \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}\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 },\n): () => void {\n const webhookHandlerRegistry = getWebhookHandlerRegistry()\n webhookHandlerRegistry.set(providerKey, {\n handler,\n queue: options?.queue,\n readSessionIdHint: options?.readSessionIdHint,\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}\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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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/widgets/extension-points.ts"],
4
- "sourcesContent": ["export type ExtensionHostFamily =\n | 'generic'\n | 'menu'\n | 'data-table'\n | 'crud-form'\n | 'detail'\n | 'portal-page'\n | 'component-handle'\n | 'entity'\n | 'api-route'\n | 'command'\n | 'event'\n | 'query-lifecycle'\n | 'dashboard'\n | 'notification'\n | 'integration'\n | 'specialized-registry'\n | 'module-override'\n\nexport type ExtensionHostCapability =\n | 'render-widget'\n | 'headless-widget'\n | 'menu-item'\n | 'column-widget'\n | 'row-action'\n | 'bulk-action'\n | 'filter-widget'\n | 'toolbar-widget'\n | 'field-widget'\n | 'lifecycle-handler'\n | 'component-replacement'\n | 'response-enricher'\n | 'query-enricher'\n | 'api-interceptor'\n | 'command-interceptor'\n | 'mutation-guard'\n | 'entity-extension'\n | 'async-subscriber'\n | 'sync-subscriber'\n | 'browser-client'\n | 'browser-portal'\n | 'registry-contribution'\n | 'module-override'\n\nexport type ExtensionHostActivation = 'always' | 'host-opt-in' | 'caller-opt-in' | 'feature-gated'\n\nexport type ExtensionPointPatternParameter = {\n source: string\n pattern?: string\n}\n\ntype ExtensionHostDeclarationBase = {\n source: string\n contextContract?: string\n dataContract?: string\n scopeContract?: string\n runtimeContract?: string\n activation?: ExtensionHostActivation\n aliases?: readonly string[]\n fallbacks?: readonly string[]\n}\n\ntype InjectionExtensionHostDeclarationBase = ExtensionHostDeclarationBase & {\n family: Exclude<ExtensionHostFamily, 'data-table' | 'crud-form' | 'component-handle'>\n supported: readonly ExtensionHostCapability[]\n}\n\nexport type InjectionExtensionHostDeclaration = InjectionExtensionHostDeclarationBase & (\n | {\n spotId: string\n pattern?: never\n parameters?: never\n }\n | {\n spotId?: never\n pattern: string\n parameters: Readonly<Record<string, ExtensionPointPatternParameter>>\n }\n)\n\nexport type DataTableExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'data-table'\n tableId: string\n baseSpotId?: string\n}\n\nexport type CrudFormExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'crud-form'\n entityId: string\n spotId?: string\n}\n\nexport type ComponentExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'component-handle'\n componentId: string\n propsContract?: string\n}\n\nexport type ModuleExtensionHostDeclaration =\n | InjectionExtensionHostDeclaration\n | DataTableExtensionHostDeclaration\n | CrudFormExtensionHostDeclaration\n | ComponentExtensionHostDeclaration\n\nexport type ModuleExtensionPoints<\n TModuleId extends string = string,\n THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>> = Readonly<Record<string, ModuleExtensionHostDeclaration>>,\n> = {\n moduleId: TModuleId\n hosts: THosts\n}\n\nexport function defineModuleExtensionPoints<\n const TModuleId extends string,\n const THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>>,\n>(declaration: ModuleExtensionPoints<TModuleId, THosts>): ModuleExtensionPoints<TModuleId, THosts> {\n return Object.freeze({\n ...declaration,\n hosts: Object.freeze({ ...declaration.hosts }),\n })\n}\n\nexport function injectionExtensionHost<const TDeclaration extends InjectionExtensionHostDeclaration>(\n declaration: TDeclaration,\n): Readonly<TDeclaration> {\n const hasExactId = typeof declaration.spotId === 'string' && declaration.spotId.length > 0\n const hasPattern = typeof declaration.pattern === 'string' && declaration.pattern.length > 0\n if (hasExactId === hasPattern) {\n throw new Error('[internal] injection extension hosts require exactly one of spotId or pattern')\n }\n if (hasPattern && (!declaration.parameters || Object.keys(declaration.parameters).length === 0)) {\n throw new Error('[internal] patterned injection extension hosts require named parameters')\n }\n return Object.freeze({ ...declaration })\n}\n\nexport function dataTableExtensionHost<\n const TDeclaration extends Omit<DataTableExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'data-table' }> {\n return Object.freeze({ family: 'data-table', ...declaration })\n}\n\nexport function crudFormExtensionHost<\n const TDeclaration extends Omit<CrudFormExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'crud-form' }> {\n return Object.freeze({ family: 'crud-form', ...declaration })\n}\n\nexport function componentExtensionHost<\n const TDeclaration extends Omit<ComponentExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'component-handle' }> {\n return Object.freeze({ family: 'component-handle', ...declaration })\n}\n\nexport type BoundExtensionSurface = {\n key: string\n suffix: string | null\n capabilities: readonly ExtensionHostCapability[]\n bound: boolean\n phases?: readonly string[]\n operations?: readonly string[]\n}\n\nexport const DATA_TABLE_EXTENSION_SURFACES = [\n { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },\n { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: true },\n { key: 'toolbar', suffix: 'toolbar', capabilities: ['toolbar-widget'], bound: true },\n { key: 'searchTrailing', suffix: 'search-trailing', capabilities: ['render-widget'], bound: true },\n { key: 'columns', suffix: 'columns', capabilities: ['column-widget'], bound: true },\n { key: 'rowActions', suffix: 'row-actions', capabilities: ['row-action'], bound: true },\n { key: 'bulkActions', suffix: 'bulk-actions', capabilities: ['bulk-action'], bound: true },\n { key: 'filters', suffix: 'filters', capabilities: ['filter-widget'], bound: true },\n { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },\n { key: 'emptyState', suffix: 'empty-state', capabilities: ['render-widget'], bound: false },\n] as const satisfies readonly BoundExtensionSurface[]\n\nexport const CRUD_FORM_EXTENSION_SURFACES = [\n { key: 'base', suffix: null, capabilities: ['render-widget', 'lifecycle-handler'], bound: true },\n { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },\n { key: 'fields', suffix: 'fields', capabilities: ['field-widget'], bound: true },\n { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },\n { key: 'beforeFields', suffix: 'before-fields', capabilities: ['render-widget'], bound: false },\n { key: 'afterFields', suffix: 'after-fields', capabilities: ['render-widget'], bound: false },\n { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: false },\n { key: 'sidebar', suffix: 'sidebar', capabilities: ['render-widget'], bound: false },\n { key: 'group', suffix: 'group:{groupId}', capabilities: ['render-widget'], bound: false },\n { key: 'fieldBefore', suffix: 'field:{fieldId}:before', capabilities: ['render-widget'], bound: false },\n { key: 'fieldAfter', suffix: 'field:{fieldId}:after', capabilities: ['render-widget'], bound: false },\n] as const satisfies readonly BoundExtensionSurface[]\n\nexport const CRUD_FORM_LIFECYCLE_PHASES = [\n 'transformValidation',\n 'transformDisplayData',\n 'onBeforeNavigate',\n 'onAppEvent',\n 'onVisibilityChange',\n 'onBeforeDelete',\n 'onDelete',\n 'onAfterDelete',\n 'onDeleteError',\n 'onFieldChange',\n 'transformFormData',\n 'onBeforeSave',\n 'onSave',\n 'onAfterSave',\n] as const\n\nexport const CRUD_FORM_OPERATIONS = ['create', 'update', 'delete'] as const\n\nexport function dataTableExtensionSpotId(tableId: string, suffix?: string): string {\n return suffix ? `data-table:${tableId}:${suffix}` : `data-table:${tableId}`\n}\n\nexport function crudFormExtensionSpotId(entityId: string, suffix?: string): string {\n return suffix ? `crud-form:${entityId}:${suffix}` : `crud-form:${entityId}`\n}\n\nexport function extensionSpotChildId(spotId: string, suffix: string): string {\n return `${spotId}:${suffix}`\n}\n\nexport function resolveExtensionPointPattern(\n pattern: string,\n parameters: Readonly<Record<string, string>>,\n): string {\n return pattern.replace(/\\{([^}]+)\\}/g, (token, parameterName: string) => parameters[parameterName] ?? token)\n}\n\nexport type ModuleExtensionSurfaceFacts = {\n hosts: ModuleExtensionHostFact[]\n contributions: ModuleExtensionContributionFact[]\n unresolved: ModuleExtensionUnresolvedFact[]\n}\n\nexport type ModuleExtensionHostFact = {\n key: string\n id: string\n resolution: 'exact' | 'pattern' | 'framework' | 'fact-ref'\n family: ExtensionHostFamily\n ownerModule: string\n capabilities: ExtensionHostCapability[]\n phases?: string[]\n operations?: string[]\n contextContract?: string\n dataContract?: string\n scopeContract?: string\n runtimeContract?: string\n activation?: ExtensionHostActivation\n bound: boolean\n stability: 'frozen' | 'stable'\n source:\n | { kind: 'declaration'; path: string; symbol: string }\n | { kind: 'fact-ref'; factSection: string; factKey: string }\n | { kind: 'framework'; path: string; symbol: string }\n aliases?: string[]\n patternParameters?: Record<string, ExtensionPointPatternParameter>\n fallbacks?: string[]\n}\n\nexport type ModuleExtensionTargetFact = {\n id: string\n resolution: 'exact' | 'pattern' | 'framework' | 'fact-ref' | 'optional-external' | 'unresolved'\n factRef?: { factSection: string; factKey: string }\n optionalOwnerPackage?: string\n}\n\nexport type ModuleExtensionContributionBase = {\n id: string\n targets: ModuleExtensionTargetFact[]\n phases?: string[]\n operations?: string[]\n features?: string[]\n scopeContract: string\n activation?: ExtensionHostActivation\n placement?: { relativeTo?: string; position?: 'first' | 'last' | 'before' | 'after'; priority?: number }\n roundTripId?: string\n override?: { domain: string; key: string; mode: 'disable-replace' | 'replace' | 'additive' }\n source: { path: string; symbol?: string }\n}\n\nexport type ModuleExtensionContributionFact = ModuleExtensionContributionBase & (\n | {\n kind: 'widget'\n details: {\n payload: 'render' | 'headless' | 'menu' | 'dashboard' | 'notification' | 'integration'\n registryKey: string\n itemIds?: string[]\n labelKeys?: string[]\n contextContract?: string\n dataContract?: string\n executionGuard: 'host' | 'contribution' | 'both'\n }\n }\n | {\n kind: 'data-table'\n details: {\n payload: 'column' | 'row-action' | 'bulk-action' | 'filter' | 'toolbar' | 'render'\n tableId: string\n executionGuard: 'host' | 'contribution' | 'both'\n }\n }\n | {\n kind: 'crud-form'\n details: {\n payload: 'render' | 'field' | 'lifecycle-handler'\n entityId: string\n fieldIds?: string[]\n groupIds?: string[]\n requestHeaderCapability: boolean\n }\n }\n | {\n kind: 'component-override'\n details: { handle: string; mode: 'replace' | 'wrapper' | 'props'; propsContract: string }\n }\n | {\n kind: 'response-enricher'\n details: {\n targetEntity: string\n surfaces: Array<'list' | 'detail'>\n timeoutMs: number\n fallback: 'none' | 'configured'\n critical: boolean\n cachePosture: 'record-pure' | 'rerun-on-list-cache-hit'\n queryEngine?: {\n engines: string[]\n applyOn: Array<'list' | 'detail'>\n activation: 'caller-opt-in'\n }\n }\n }\n | {\n kind: 'api-interceptor'\n details: {\n route: string\n methods: string[]\n phases: Array<'before' | 'after'>\n activation: 'crud-pipeline' | 'custom-route-bridge'\n timeoutMs: number\n failurePosture: 'fail-closed' | 'fallback'\n }\n }\n | {\n kind: 'command-interceptor'\n details: {\n targetCommand: string\n phases: Array<'before-execute' | 'after-execute' | 'before-undo' | 'after-undo'>\n }\n }\n | {\n kind: 'mutation-guard'\n details: {\n entityId: string\n operations: Array<'create' | 'update' | 'delete'>\n capabilities: Array<'block' | 'rewrite' | 'after-success'>\n optimisticLock: 'preserved'\n }\n }\n | {\n kind: 'entity-extension'\n details: {\n hostEntityId: string\n extensionEntityId: string\n linkId: string\n scopeContract: string\n orphanContract: string\n }\n }\n | {\n kind: 'subscriber'\n details: {\n event: string\n subscriberId: string\n persistent: boolean\n sync: boolean\n priority?: number\n }\n }\n | {\n kind: 'browser-reaction'\n details: {\n transports: Array<'client' | 'portal' | 'notification-effect'>\n hooks: string[]\n audienceScopeContract: string\n maxPayloadBytes?: number\n dedupWindowMs?: number\n }\n }\n | {\n kind: 'specialized-registry'\n details: {\n registry: 'notification' | 'integration' | 'search' | 'vector' | 'ai' | 'payment' | 'shipping' | 'currency' | 'workflow'\n registryId: string\n specialistRoute: string\n }\n }\n | {\n kind: 'module-override'\n details: {\n domain: string\n key: string\n mode: 'disable-replace' | 'replace' | 'additive'\n }\n }\n)\n\nexport type ModuleExtensionUnresolvedFact = {\n key: string\n source: { path: string; symbol?: string }\n reason:\n | 'unclassified-binding'\n | 'unbound-declaration'\n | 'dynamic-without-pattern'\n | 'unresolved-first-party-target'\n}\n"],
4
+ "sourcesContent": ["export type ExtensionHostFamily =\n | 'generic'\n | 'menu'\n | 'data-table'\n | 'crud-form'\n | 'detail'\n | 'portal-page'\n | 'component-handle'\n | 'entity'\n | 'api-route'\n | 'command'\n | 'event'\n | 'query-lifecycle'\n | 'dashboard'\n | 'notification'\n | 'integration'\n | 'specialized-registry'\n | 'module-override'\n\nexport type ExtensionHostCapability =\n | 'render-widget'\n | 'headless-widget'\n | 'menu-item'\n | 'column-widget'\n | 'row-action'\n | 'bulk-action'\n | 'filter-widget'\n | 'toolbar-widget'\n | 'field-widget'\n | 'lifecycle-handler'\n | 'component-replacement'\n | 'response-enricher'\n | 'query-enricher'\n | 'api-interceptor'\n | 'command-interceptor'\n | 'mutation-guard'\n | 'entity-extension'\n | 'async-subscriber'\n | 'sync-subscriber'\n | 'browser-client'\n | 'browser-portal'\n | 'registry-contribution'\n | 'module-override'\n\nexport type ExtensionHostActivation = 'always' | 'host-opt-in' | 'caller-opt-in' | 'feature-gated'\n\nexport type ExtensionPointPatternParameter = {\n source: string\n pattern?: string\n}\n\ntype ExtensionHostDeclarationBase = {\n source: string\n contextContract?: string\n dataContract?: string\n scopeContract?: string\n runtimeContract?: string\n activation?: ExtensionHostActivation\n aliases?: readonly string[]\n fallbacks?: readonly string[]\n}\n\ntype InjectionExtensionHostDeclarationBase = ExtensionHostDeclarationBase & {\n family: Exclude<ExtensionHostFamily, 'data-table' | 'crud-form' | 'component-handle'>\n supported: readonly ExtensionHostCapability[]\n}\n\nexport type InjectionExtensionHostDeclaration = InjectionExtensionHostDeclarationBase & (\n | {\n spotId: string\n pattern?: never\n parameters?: never\n }\n | {\n spotId?: never\n pattern: string\n parameters: Readonly<Record<string, ExtensionPointPatternParameter>>\n }\n)\n\nexport type DataTableExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'data-table'\n tableId: string\n baseSpotId?: string\n}\n\nexport type CrudFormExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'crud-form'\n entityId: string\n spotId?: string\n}\n\nexport type ComponentExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'component-handle'\n componentId: string\n propsContract?: string\n}\n\nexport type ModuleExtensionHostDeclaration =\n | InjectionExtensionHostDeclaration\n | DataTableExtensionHostDeclaration\n | CrudFormExtensionHostDeclaration\n | ComponentExtensionHostDeclaration\n\nexport type ModuleExtensionPoints<\n TModuleId extends string = string,\n THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>> = Readonly<Record<string, ModuleExtensionHostDeclaration>>,\n> = {\n moduleId: TModuleId\n hosts: THosts\n}\n\nexport function defineModuleExtensionPoints<\n const TModuleId extends string,\n const THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>>,\n>(declaration: ModuleExtensionPoints<TModuleId, THosts>): ModuleExtensionPoints<TModuleId, THosts> {\n return Object.freeze({\n ...declaration,\n hosts: Object.freeze({ ...declaration.hosts }),\n })\n}\n\nexport function injectionExtensionHost<const TDeclaration extends InjectionExtensionHostDeclaration>(\n declaration: TDeclaration,\n): Readonly<TDeclaration> {\n const hasExactId = typeof declaration.spotId === 'string' && declaration.spotId.length > 0\n const hasPattern = typeof declaration.pattern === 'string' && declaration.pattern.length > 0\n if (hasExactId === hasPattern) {\n throw new Error('[internal] injection extension hosts require exactly one of spotId or pattern')\n }\n if (hasPattern && (!declaration.parameters || Object.keys(declaration.parameters).length === 0)) {\n throw new Error('[internal] patterned injection extension hosts require named parameters')\n }\n return Object.freeze({ ...declaration })\n}\n\nexport function dataTableExtensionHost<\n const TDeclaration extends Omit<DataTableExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'data-table' }> {\n return Object.freeze({ family: 'data-table', ...declaration })\n}\n\nexport function crudFormExtensionHost<\n const TDeclaration extends Omit<CrudFormExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'crud-form' }> {\n return Object.freeze({ family: 'crud-form', ...declaration })\n}\n\nexport function componentExtensionHost<\n const TDeclaration extends Omit<ComponentExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'component-handle' }> {\n return Object.freeze({ family: 'component-handle', ...declaration })\n}\n\nexport type BoundExtensionSurface = {\n key: string\n suffix: string | null\n capabilities: readonly ExtensionHostCapability[]\n bound: boolean\n phases?: readonly string[]\n operations?: readonly string[]\n}\n\nexport const DATA_TABLE_EXTENSION_SURFACES = [\n { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },\n { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: true },\n { key: 'toolbar', suffix: 'toolbar', capabilities: ['toolbar-widget'], bound: true },\n { key: 'searchTrailing', suffix: 'search-trailing', capabilities: ['render-widget'], bound: true },\n { key: 'columns', suffix: 'columns', capabilities: ['column-widget'], bound: true },\n { key: 'rowActions', suffix: 'row-actions', capabilities: ['row-action'], bound: true },\n { key: 'bulkActions', suffix: 'bulk-actions', capabilities: ['bulk-action'], bound: true },\n { key: 'filters', suffix: 'filters', capabilities: ['filter-widget'], bound: true },\n { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },\n { key: 'emptyState', suffix: 'empty-state', capabilities: ['render-widget'], bound: false },\n] as const satisfies readonly BoundExtensionSurface[]\n\nexport const CRUD_FORM_EXTENSION_SURFACES = [\n { key: 'base', suffix: null, capabilities: ['render-widget', 'lifecycle-handler'], bound: true },\n { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },\n { key: 'fields', suffix: 'fields', capabilities: ['field-widget'], bound: true },\n { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },\n { key: 'beforeFields', suffix: 'before-fields', capabilities: ['render-widget'], bound: false },\n { key: 'afterFields', suffix: 'after-fields', capabilities: ['render-widget'], bound: false },\n { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: false },\n { key: 'sidebar', suffix: 'sidebar', capabilities: ['render-widget'], bound: false },\n { key: 'group', suffix: 'group:{groupId}', capabilities: ['render-widget'], bound: false },\n { key: 'fieldBefore', suffix: 'field:{fieldId}:before', capabilities: ['render-widget'], bound: false },\n { key: 'fieldAfter', suffix: 'field:{fieldId}:after', capabilities: ['render-widget'], bound: false },\n] as const satisfies readonly BoundExtensionSurface[]\n\nexport const CRUD_FORM_LIFECYCLE_PHASES = [\n 'transformValidation',\n 'transformDisplayData',\n 'onBeforeNavigate',\n 'onAppEvent',\n 'onVisibilityChange',\n 'onBeforeDelete',\n 'onDelete',\n 'onAfterDelete',\n 'onDeleteError',\n 'onFieldChange',\n 'transformFormData',\n 'onBeforeSave',\n 'onSave',\n 'onAfterSave',\n] as const\n\nexport const CRUD_FORM_OPERATIONS = ['create', 'update', 'delete'] as const\n\nexport function dataTableExtensionSpotId(tableId: string, suffix?: string): string {\n return suffix ? `data-table:${tableId}:${suffix}` : `data-table:${tableId}`\n}\n\nexport function crudFormExtensionSpotId(entityId: string, suffix?: string): string {\n return suffix ? `crud-form:${entityId}:${suffix}` : `crud-form:${entityId}`\n}\n\nexport function extensionSpotChildId(spotId: string, suffix: string): string {\n return `${spotId}:${suffix}`\n}\n\nexport function resolveExtensionPointPattern(\n pattern: string,\n parameters: Readonly<Record<string, string>>,\n): string {\n return pattern.replace(/\\{([^}]+)\\}/g, (token, parameterName: string) => parameters[parameterName] ?? token)\n}\n\n/**\n * Portable, structural copy of the CLI provenance source reference\n * (`ModuleFactSourceRef` in `packages/cli/.../module-facts.ts`). Redefined here\n * \u2014 rather than imported \u2014 so `@open-mercato/shared` keeps zero dependency on\n * `@open-mercato/cli`. Field names are kept byte-identical so the two are\n * assignable across the package boundary.\n */\nexport type ModuleFactSourceRef = {\n sourcePath: string\n exportName?: string\n line?: number\n}\n\n/** Portable, structural copy of the CLI provenance fact reference. */\nexport type ModuleFactRef = {\n factSection: string\n factKey: string\n}\n\nexport type ModuleExtensionContributionKind = ModuleExtensionContributionFact['kind']\n\nexport type ModuleExtensionActivationKind =\n | 'crud-response-enricher'\n | 'query-enricher'\n | 'mutation-guard'\n | 'api-interceptor-bridge'\n | 'command-interceptor-bridge'\n | 'widget-injection-consumer'\n | 'component-extension-consumer'\n | 'dashboard-host-consumer'\n\nexport type ModuleExtensionTargetRef = {\n kind:\n | 'module'\n | 'entity'\n | 'api-route'\n | 'command'\n | 'widget-spot'\n | 'component'\n | 'event'\n | 'notification'\n | 'wildcard'\n id: string\n moduleId?: string\n method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n}\n\nexport type ModuleExtensionActivation = {\n id: string\n kind: ModuleExtensionActivationKind\n host: ModuleExtensionTargetRef\n contributionKinds: ModuleExtensionContributionKind[]\n phases?: string[]\n /**\n * Mutation operations the call site guards, when statically declared. A\n * contribution declaring only other operations is not bound by this activation.\n */\n operations?: string[]\n source: ModuleFactSourceRef\n bridge?: ModuleFactRef\n}\n\nexport type ModuleExtensionResolution =\n | 'bound'\n | 'capability-only'\n | 'optional-target-missing'\n | 'wildcard'\n | 'unresolved'\n\nexport type ModuleIncomingExtensionRef = {\n contributionId: string\n contributionKind: ModuleExtensionContributionKind\n contributorModuleId: string\n target: ModuleExtensionTargetRef\n activationId?: string\n resolution: ModuleExtensionResolution\n source: ModuleFactSourceRef\n}\n\nexport type ModuleContributionResolution = {\n contributionId: string\n target: ModuleExtensionTargetRef\n resolution: ModuleExtensionResolution\n activationIds: string[]\n}\n\n/**\n * Additive, optional bidirectional-topology fields layered on top of the\n * existing extension-surface facts. Every field is optional so legacy consumers\n * and pre-existing generated output remain byte-compatible.\n */\nexport type ModuleExtensionSurfaceFactsAdditions = {\n activations?: ModuleExtensionActivation[]\n incoming?: ModuleIncomingExtensionRef[]\n contributionResolutions?: ModuleContributionResolution[]\n}\n\nexport type ModuleExtensionSurfaceFacts = {\n hosts: ModuleExtensionHostFact[]\n contributions: ModuleExtensionContributionFact[]\n unresolved: ModuleExtensionUnresolvedFact[]\n} & ModuleExtensionSurfaceFactsAdditions\n\nexport type ModuleExtensionHostFact = {\n key: string\n id: string\n resolution: 'exact' | 'pattern' | 'framework' | 'fact-ref'\n family: ExtensionHostFamily\n ownerModule: string\n capabilities: ExtensionHostCapability[]\n phases?: string[]\n operations?: string[]\n contextContract?: string\n dataContract?: string\n scopeContract?: string\n runtimeContract?: string\n activation?: ExtensionHostActivation\n bound: boolean\n stability: 'frozen' | 'stable'\n source:\n | { kind: 'declaration'; path: string; symbol: string }\n | { kind: 'fact-ref'; factSection: string; factKey: string }\n | { kind: 'framework'; path: string; symbol: string }\n aliases?: string[]\n patternParameters?: Record<string, ExtensionPointPatternParameter>\n fallbacks?: string[]\n}\n\nexport type ModuleExtensionTargetFact = {\n id: string\n resolution: 'exact' | 'pattern' | 'framework' | 'fact-ref' | 'optional-external' | 'unresolved'\n factRef?: { factSection: string; factKey: string }\n optionalOwnerPackage?: string\n}\n\nexport type ModuleExtensionContributionBase = {\n id: string\n targets: ModuleExtensionTargetFact[]\n phases?: string[]\n operations?: string[]\n features?: string[]\n scopeContract: string\n activation?: ExtensionHostActivation\n placement?: { relativeTo?: string; position?: 'first' | 'last' | 'before' | 'after'; priority?: number }\n roundTripId?: string\n override?: { domain: string; key: string; mode: 'disable-replace' | 'replace' | 'additive' }\n source: { path: string; symbol?: string }\n}\n\nexport type ModuleExtensionContributionFact = ModuleExtensionContributionBase & (\n | {\n kind: 'widget'\n details: {\n payload: 'render' | 'headless' | 'menu' | 'dashboard' | 'notification' | 'integration'\n registryKey: string\n itemIds?: string[]\n labelKeys?: string[]\n contextContract?: string\n dataContract?: string\n executionGuard: 'host' | 'contribution' | 'both'\n }\n }\n | {\n kind: 'data-table'\n details: {\n payload: 'column' | 'row-action' | 'bulk-action' | 'filter' | 'toolbar' | 'render'\n tableId: string\n executionGuard: 'host' | 'contribution' | 'both'\n }\n }\n | {\n kind: 'crud-form'\n details: {\n payload: 'render' | 'field' | 'lifecycle-handler'\n entityId: string\n fieldIds?: string[]\n groupIds?: string[]\n requestHeaderCapability: boolean\n }\n }\n | {\n kind: 'component-override'\n details: { handle: string; mode: 'replace' | 'wrapper' | 'props'; propsContract: string }\n }\n | {\n kind: 'response-enricher'\n details: {\n targetEntity: string\n surfaces: Array<'list' | 'detail'>\n timeoutMs: number\n fallback: 'none' | 'configured'\n critical: boolean\n cachePosture: 'record-pure' | 'rerun-on-list-cache-hit'\n queryEngine?: {\n engines: string[]\n applyOn: Array<'list' | 'detail'>\n activation: 'caller-opt-in'\n }\n }\n }\n | {\n kind: 'api-interceptor'\n details: {\n route: string\n methods: string[]\n phases: Array<'before' | 'after'>\n activation: 'crud-pipeline' | 'custom-route-bridge'\n timeoutMs: number\n failurePosture: 'fail-closed' | 'fallback'\n }\n }\n | {\n kind: 'command-interceptor'\n details: {\n targetCommand: string\n phases: Array<'before-execute' | 'after-execute' | 'before-undo' | 'after-undo'>\n }\n }\n | {\n kind: 'mutation-guard'\n details: {\n entityId: string\n operations: Array<'create' | 'update' | 'delete'>\n capabilities: Array<'block' | 'rewrite' | 'after-success'>\n optimisticLock: 'preserved'\n }\n }\n | {\n kind: 'entity-extension'\n details: {\n hostEntityId: string\n extensionEntityId: string\n linkId: string\n scopeContract: string\n orphanContract: string\n }\n }\n | {\n kind: 'subscriber'\n details: {\n event: string\n subscriberId: string\n persistent: boolean\n sync: boolean\n priority?: number\n }\n }\n | {\n kind: 'browser-reaction'\n details: {\n transports: Array<'client' | 'portal' | 'notification-effect'>\n hooks: string[]\n audienceScopeContract: string\n maxPayloadBytes?: number\n dedupWindowMs?: number\n /**\n * Statically declared handler id. Runtime keys `notifications.handlers`\n * overrides by it, so it is absent when the module declares no id (such a\n * handler cannot be overridden).\n */\n overrideKey?: string\n }\n }\n | {\n kind: 'specialized-registry'\n details: {\n registry: 'notification' | 'integration' | 'search' | 'vector' | 'ai' | 'payment' | 'shipping' | 'currency' | 'workflow'\n registryId: string\n specialistRoute: string\n }\n }\n | {\n kind: 'module-override'\n details: {\n domain: string\n key: string\n mode: 'disable-replace' | 'replace' | 'additive'\n }\n }\n)\n\nexport type ModuleExtensionUnresolvedFact = {\n key: string\n source: { path: string; symbol?: string }\n reason:\n | 'unclassified-binding'\n | 'unbound-declaration'\n | 'dynamic-without-pattern'\n | 'unresolved-first-party-target'\n}\n"],
5
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.6.7-develop.6795.1.8a3f27921c",
3
+ "version": "0.6.7-develop.6814.1.0627c7e9f1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -105,7 +105,7 @@
105
105
  "@mikro-orm/core": "^7.1.8",
106
106
  "@mikro-orm/decorators": "^7.1.8",
107
107
  "@mikro-orm/postgresql": "^7.1.8",
108
- "@open-mercato/cache": "0.6.7-develop.6795.1.8a3f27921c",
108
+ "@open-mercato/cache": "0.6.7-develop.6814.1.0627c7e9f1",
109
109
  "@types/sanitize-html": "^2.16.1",
110
110
  "dotenv": "^17.4.2",
111
111
  "pino": "^10.3.1",
@@ -0,0 +1,139 @@
1
+ // The app-level DI override module (`@/di`) is OPTIONAL: most apps ship no
2
+ // src/di.ts, and packages contexts cannot resolve the alias at all. Its
3
+ // absence must not produce warnings — only real load/register failures do
4
+ // (covered in container-app-di.test.ts). This file deliberately does NOT
5
+ // mock '@/di', so the import fails with module-not-found, exercising the
6
+ // quiet branch.
7
+
8
+ jest.mock(
9
+ '@open-mercato/shared/lib/db/mikro',
10
+ () => {
11
+ const baseEm: any = {
12
+ fork: () => baseEm,
13
+ getRepository: () => ({ find: async () => [], findOne: async () => null }),
14
+ }
15
+ return {
16
+ __esModule: true,
17
+ getOrm: async () => ({ em: baseEm }),
18
+ getOrmEntities: () => [],
19
+ registerOrmEntities: () => {},
20
+ }
21
+ },
22
+ { virtual: false },
23
+ )
24
+
25
+ jest.mock(
26
+ '@mikro-orm/core',
27
+ () => ({
28
+ __esModule: true,
29
+ RequestContext: { getEntityManager: () => null },
30
+ }),
31
+ { virtual: true },
32
+ )
33
+
34
+ jest.mock(
35
+ '@open-mercato/shared/lib/query/engine',
36
+ () => ({ __esModule: true, BasicQueryEngine: class {} }),
37
+ { virtual: false },
38
+ )
39
+
40
+ jest.mock(
41
+ '@open-mercato/shared/lib/data/engine',
42
+ () => ({ __esModule: true, DefaultDataEngine: class {} }),
43
+ { virtual: false },
44
+ )
45
+
46
+ jest.mock(
47
+ '@open-mercato/shared/lib/commands',
48
+ () => ({
49
+ __esModule: true,
50
+ commandRegistry: {},
51
+ CommandBus: class { constructor() {} },
52
+ }),
53
+ { virtual: false },
54
+ )
55
+
56
+ jest.mock(
57
+ '@open-mercato/shared/modules/overrides',
58
+ () => ({
59
+ __esModule: true,
60
+ applyDiOverridesToContainer: () => {},
61
+ }),
62
+ { virtual: false },
63
+ )
64
+
65
+ // Keep this suite hermetic: without this mock the REAL core bootstrap module
66
+ // loads (and runs its module-level side effects) whenever generated files
67
+ // exist on disk, polluting globalThis state shared with sibling test files.
68
+ jest.mock(
69
+ '@open-mercato/core/bootstrap',
70
+ () => ({
71
+ __esModule: true,
72
+ bootstrap: async () => {},
73
+ }),
74
+ { virtual: true },
75
+ )
76
+
77
+ jest.mock(
78
+ '@open-mercato/shared/lib/encryption/subscriber',
79
+ () => ({
80
+ __esModule: true,
81
+ registerTenantEncryptionSubscriber: () => {},
82
+ }),
83
+ { virtual: true },
84
+ )
85
+
86
+ const mockWarn = jest.fn()
87
+ jest.mock('../../logger', () => ({
88
+ createLogger: () => ({
89
+ debug: jest.fn(),
90
+ info: jest.fn(),
91
+ warn: (...args: unknown[]) => mockWarn(...args),
92
+ error: jest.fn(),
93
+ child() { return this },
94
+ }),
95
+ }))
96
+
97
+ const {
98
+ createRequestContainer,
99
+ registerAppDiRegistrar,
100
+ registerDiRegistrars,
101
+ resetBootstrapCache,
102
+ } = require('@open-mercato/shared/lib/di/container')
103
+
104
+ describe('app-level DI override hook when @/di is absent', () => {
105
+ beforeEach(() => {
106
+ resetBootstrapCache()
107
+ registerAppDiRegistrar(null)
108
+ registerDiRegistrars([])
109
+ mockWarn.mockClear()
110
+ })
111
+
112
+ it('creates the container without warning', async () => {
113
+ const firstContainer = await createRequestContainer()
114
+ const secondContainer = await createRequestContainer()
115
+ expect(firstContainer).toBeDefined()
116
+ expect(secondContainer).toBeDefined()
117
+ expect(mockWarn).not.toHaveBeenCalled()
118
+ })
119
+
120
+ it('warns once when @/di fails because a nested app alias is missing', async () => {
121
+ const nestedAliasError = Object.assign(new Error("Cannot find module '@/di/helpers'"), {
122
+ code: 'MODULE_NOT_FOUND',
123
+ })
124
+ jest.doMock('@/di', () => {
125
+ throw nestedAliasError
126
+ }, { virtual: true })
127
+
128
+ const firstContainer = await createRequestContainer()
129
+ const secondContainer = await createRequestContainer()
130
+
131
+ expect(firstContainer).toBeDefined()
132
+ expect(secondContainer).toBeDefined()
133
+ expect(mockWarn).toHaveBeenCalledTimes(1)
134
+ expect(mockWarn).toHaveBeenCalledWith(
135
+ 'App-level DI override module (@/di) failed to load; its registrations are skipped',
136
+ { err: nestedAliasError },
137
+ )
138
+ })
139
+ })
@@ -0,0 +1,173 @@
1
+ // Tests for the app-level DI override hook (`src/di.ts` → `@/di`) in
2
+ // `createRequestContainer()`. The hook used to swallow EVERY failure with
3
+ // bare `catch {}` blocks, so a src/di.ts whose register() threw — or a @/di
4
+ // module that failed to load for any reason other than being absent — was
5
+ // skipped without a trace and the documented override point silently did
6
+ // nothing. Absence of the optional module must stay quiet; real failures
7
+ // must be logged.
8
+
9
+ import { asValue } from 'awilix'
10
+
11
+ jest.mock(
12
+ '@open-mercato/shared/lib/db/mikro',
13
+ () => {
14
+ const baseEm: any = {
15
+ fork: () => baseEm,
16
+ getRepository: () => ({ find: async () => [], findOne: async () => null }),
17
+ }
18
+ return {
19
+ __esModule: true,
20
+ getOrm: async () => ({ em: baseEm }),
21
+ getOrmEntities: () => [],
22
+ registerOrmEntities: () => {},
23
+ }
24
+ },
25
+ { virtual: false },
26
+ )
27
+
28
+ jest.mock(
29
+ '@mikro-orm/core',
30
+ () => ({
31
+ __esModule: true,
32
+ RequestContext: { getEntityManager: () => null },
33
+ }),
34
+ { virtual: true },
35
+ )
36
+
37
+ jest.mock(
38
+ '@open-mercato/shared/lib/query/engine',
39
+ () => ({ __esModule: true, BasicQueryEngine: class {} }),
40
+ { virtual: false },
41
+ )
42
+
43
+ jest.mock(
44
+ '@open-mercato/shared/lib/data/engine',
45
+ () => ({ __esModule: true, DefaultDataEngine: class {} }),
46
+ { virtual: false },
47
+ )
48
+
49
+ jest.mock(
50
+ '@open-mercato/shared/lib/commands',
51
+ () => ({
52
+ __esModule: true,
53
+ commandRegistry: {},
54
+ CommandBus: class { constructor() {} },
55
+ }),
56
+ { virtual: false },
57
+ )
58
+
59
+ jest.mock(
60
+ '@open-mercato/shared/modules/overrides',
61
+ () => ({
62
+ __esModule: true,
63
+ applyDiOverridesToContainer: () => {},
64
+ }),
65
+ { virtual: false },
66
+ )
67
+
68
+ // Keep this suite hermetic: without this mock the REAL core bootstrap module
69
+ // loads (and runs its module-level side effects) whenever generated files
70
+ // exist on disk, polluting globalThis state shared with sibling test files.
71
+ jest.mock(
72
+ '@open-mercato/core/bootstrap',
73
+ () => ({
74
+ __esModule: true,
75
+ bootstrap: async () => {},
76
+ }),
77
+ { virtual: true },
78
+ )
79
+
80
+ jest.mock(
81
+ '@open-mercato/shared/lib/encryption/subscriber',
82
+ () => ({
83
+ __esModule: true,
84
+ registerTenantEncryptionSubscriber: () => {},
85
+ }),
86
+ { virtual: true },
87
+ )
88
+
89
+ const mockWarn = jest.fn()
90
+ jest.mock('../../logger', () => ({
91
+ createLogger: () => ({
92
+ debug: jest.fn(),
93
+ info: jest.fn(),
94
+ warn: (...args: unknown[]) => mockWarn(...args),
95
+ error: jest.fn(),
96
+ child() { return this },
97
+ }),
98
+ }))
99
+
100
+ const mockAppDiRegister = jest.fn()
101
+ jest.mock(
102
+ '@/di',
103
+ () => ({
104
+ __esModule: true,
105
+ register: (...args: unknown[]) => mockAppDiRegister(...args),
106
+ }),
107
+ { virtual: true },
108
+ )
109
+
110
+ const {
111
+ createRequestContainer,
112
+ registerAppDiRegistrar,
113
+ registerDiRegistrars,
114
+ resetBootstrapCache,
115
+ } = require('@open-mercato/shared/lib/di/container')
116
+
117
+ describe('app-level DI override hook (@/di)', () => {
118
+ beforeEach(() => {
119
+ resetBootstrapCache()
120
+ registerAppDiRegistrar(null)
121
+ registerDiRegistrars([])
122
+ mockWarn.mockClear()
123
+ mockAppDiRegister.mockReset()
124
+ })
125
+
126
+ it('calls register() with the request container and applies its registrations', async () => {
127
+ mockAppDiRegister.mockImplementation((container: any) => {
128
+ container.register({ appOverrideProbe: asValue('from-app-di') })
129
+ })
130
+ const container = await createRequestContainer()
131
+ expect(mockAppDiRegister).toHaveBeenCalledTimes(1)
132
+ expect(mockAppDiRegister).toHaveBeenCalledWith(container)
133
+ expect(container.resolve('appOverrideProbe')).toBe('from-app-di')
134
+ expect(mockWarn).not.toHaveBeenCalled()
135
+ })
136
+
137
+ it('awaits an async register()', async () => {
138
+ mockAppDiRegister.mockImplementation(async (container: any) => {
139
+ await Promise.resolve()
140
+ container.register({ appOverrideProbe: asValue('async-app-di') })
141
+ })
142
+ const container = await createRequestContainer()
143
+ expect(container.resolve('appOverrideProbe')).toBe('async-app-di')
144
+ expect(mockWarn).not.toHaveBeenCalled()
145
+ })
146
+
147
+ it('warns instead of silently swallowing when register() throws', async () => {
148
+ mockAppDiRegister.mockImplementation(() => {
149
+ throw new Error('boom from app di')
150
+ })
151
+ const firstContainer = await createRequestContainer()
152
+ const secondContainer = await createRequestContainer()
153
+ expect(firstContainer).toBeDefined()
154
+ expect(secondContainer).toBeDefined()
155
+ expect(mockAppDiRegister).toHaveBeenCalledTimes(2)
156
+ expect(mockWarn).toHaveBeenCalledTimes(1)
157
+ const [message, fields] = mockWarn.mock.calls[0]
158
+ expect(String(message)).toContain('register()')
159
+ expect((fields as { err: Error }).err.message).toBe('boom from app di')
160
+ })
161
+
162
+ it('warns when an async register() rejects', async () => {
163
+ mockAppDiRegister.mockImplementation(async () => {
164
+ throw new Error('async boom from app di')
165
+ })
166
+ const firstContainer = await createRequestContainer()
167
+ const secondContainer = await createRequestContainer()
168
+ expect(firstContainer).toBeDefined()
169
+ expect(secondContainer).toBeDefined()
170
+ expect(mockAppDiRegister).toHaveBeenCalledTimes(2)
171
+ expect(mockWarn).toHaveBeenCalledTimes(1)
172
+ })
173
+ })