@open-mercato/shared 0.6.7-develop.6788.1.c2a1520a30 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -0
- package/dist/lib/auth/organizationScope.js +34 -0
- package/dist/lib/auth/organizationScope.js.map +7 -0
- package/dist/lib/di/container.js +33 -3
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/email/send.js +56 -0
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/query/engine.js +25 -42
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/search/availability.js +107 -0
- package/dist/lib/search/availability.js.map +7 -0
- package/dist/lib/search/config.js +24 -2
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenize.js +23 -11
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +57 -0
- package/dist/lib/webhooks/body.js.map +7 -0
- package/dist/lib/webhooks/index.js +13 -1
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/lib/webhooks/verify.js +12 -7
- package/dist/lib/webhooks/verify.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +8 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/widgets/extension-points.js.map +1 -1
- package/package.json +8 -8
- package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
- package/src/lib/auth/organizationScope.ts +65 -0
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +139 -0
- package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
- package/src/lib/di/container.ts +45 -4
- package/src/lib/email/__tests__/send.test.ts +37 -1
- package/src/lib/email/send.ts +79 -0
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
- package/src/lib/query/__tests__/engine.test.ts +65 -13
- package/src/lib/query/engine.ts +36 -60
- package/src/lib/query/types.ts +4 -5
- package/src/lib/search/__tests__/availability.test.ts +211 -0
- package/src/lib/search/__tests__/config.test.ts +59 -0
- package/src/lib/search/__tests__/tokenize.test.ts +49 -0
- package/src/lib/search/availability.ts +232 -0
- package/src/lib/search/config.ts +28 -0
- package/src/lib/search/tokenize.ts +33 -11
- package/src/lib/webhooks/__tests__/body.test.ts +95 -0
- package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
- package/src/lib/webhooks/body.ts +70 -0
- package/src/lib/webhooks/index.ts +7 -1
- package/src/lib/webhooks/verify.ts +17 -9
- package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
- package/src/modules/payment_gateways/types.ts +15 -0
- package/src/modules/widgets/extension-points.ts +104 -1
|
@@ -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": "
|
|
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.
|
|
3
|
+
"version": "0.6.7-develop.6814.1.0627c7e9f1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -102,23 +102,23 @@
|
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@mikro-orm/core": "^7.1.
|
|
106
|
-
"@mikro-orm/decorators": "^7.1.
|
|
107
|
-
"@mikro-orm/postgresql": "^7.1.
|
|
108
|
-
"@open-mercato/cache": "0.6.7-develop.
|
|
105
|
+
"@mikro-orm/core": "^7.1.8",
|
|
106
|
+
"@mikro-orm/decorators": "^7.1.8",
|
|
107
|
+
"@mikro-orm/postgresql": "^7.1.8",
|
|
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",
|
|
112
112
|
"rate-limiter-flexible": "^11.2.0",
|
|
113
113
|
"re2js": "2.8.6",
|
|
114
114
|
"reflect-metadata": "^0.2.2",
|
|
115
|
-
"sanitize-html": "
|
|
116
|
-
"undici": "^8.
|
|
115
|
+
"sanitize-html": "2.17.5",
|
|
116
|
+
"undici": "^8.9.0"
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@types/jest": "^30.0.0",
|
|
120
120
|
"jest": "^30.4.2",
|
|
121
|
-
"ts-jest": "^29.4.
|
|
121
|
+
"ts-jest": "^29.4.12",
|
|
122
122
|
"ts-morph": "^28.0.0",
|
|
123
123
|
"typescript": "7.0.2"
|
|
124
124
|
},
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,
|
|
5
|
+
organizationScopeRequiredResponse,
|
|
6
|
+
resolveActiveOrganizationId,
|
|
7
|
+
} from '../organizationScope'
|
|
8
|
+
|
|
9
|
+
const accountOrgId = '22222222-2222-4222-8222-222222222222'
|
|
10
|
+
const selectedOrgId = '33333333-3333-4333-8333-333333333333'
|
|
11
|
+
const actorTenantId = '44444444-4444-4444-8444-444444444444'
|
|
12
|
+
const foreignTenantId = '55555555-5555-4555-8555-555555555555'
|
|
13
|
+
|
|
14
|
+
describe('resolveActiveOrganizationId', () => {
|
|
15
|
+
it('uses the selected organization when one is set', () => {
|
|
16
|
+
expect(
|
|
17
|
+
resolveActiveOrganizationId({ orgId: selectedOrgId, actorOrgId: accountOrgId }),
|
|
18
|
+
).toBe(selectedOrgId)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// `orgId: null` + `actorOrgId` set is exactly the shape `applySuperAdminScope` produces for an
|
|
22
|
+
// all-organizations selection. Answering 401 for it sent `apiFetch` into a refresh loop.
|
|
23
|
+
it('falls back to the actor organization for an all-organizations selection', () => {
|
|
24
|
+
expect(
|
|
25
|
+
resolveActiveOrganizationId({ orgId: null, actorOrgId: accountOrgId }),
|
|
26
|
+
).toBe(accountOrgId)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// `actorTenantId` is only present when the super-admin cookie override switched tenants.
|
|
30
|
+
// Selecting the actor's own tenant explicitly keeps the fallback valid.
|
|
31
|
+
it('keeps the fallback when the tenant override selects the actor tenant', () => {
|
|
32
|
+
expect(
|
|
33
|
+
resolveActiveOrganizationId({
|
|
34
|
+
orgId: null,
|
|
35
|
+
actorOrgId: accountOrgId,
|
|
36
|
+
tenantId: actorTenantId,
|
|
37
|
+
actorTenantId,
|
|
38
|
+
}),
|
|
39
|
+
).toBe(accountOrgId)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// The reviewed blocker: tenant B + "all organizations" must not pair the actor's
|
|
43
|
+
// tenant-A organization with tenant B. No fallback — the route answers 400 instead.
|
|
44
|
+
it('refuses the fallback when the effective tenant is not the actor tenant', () => {
|
|
45
|
+
expect(
|
|
46
|
+
resolveActiveOrganizationId({
|
|
47
|
+
orgId: null,
|
|
48
|
+
actorOrgId: accountOrgId,
|
|
49
|
+
tenantId: foreignTenantId,
|
|
50
|
+
actorTenantId,
|
|
51
|
+
}),
|
|
52
|
+
).toBeNull()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('refuses the fallback when the tenant override cleared the tenant entirely', () => {
|
|
56
|
+
expect(
|
|
57
|
+
resolveActiveOrganizationId({
|
|
58
|
+
orgId: null,
|
|
59
|
+
actorOrgId: accountOrgId,
|
|
60
|
+
tenantId: null,
|
|
61
|
+
actorTenantId,
|
|
62
|
+
}),
|
|
63
|
+
).toBeNull()
|
|
64
|
+
expect(
|
|
65
|
+
resolveActiveOrganizationId({
|
|
66
|
+
orgId: null,
|
|
67
|
+
actorOrgId: accountOrgId,
|
|
68
|
+
tenantId: foreignTenantId,
|
|
69
|
+
actorTenantId: null,
|
|
70
|
+
}),
|
|
71
|
+
).toBeNull()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('returns null when the caller has no organization at all', () => {
|
|
75
|
+
expect(resolveActiveOrganizationId({ orgId: null })).toBeNull()
|
|
76
|
+
expect(resolveActiveOrganizationId({ orgId: null, actorOrgId: null })).toBeNull()
|
|
77
|
+
expect(resolveActiveOrganizationId(null)).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('ignores blank and non-string values rather than scoping to them', () => {
|
|
81
|
+
expect(resolveActiveOrganizationId({ orgId: ' ', actorOrgId: accountOrgId })).toBe(accountOrgId)
|
|
82
|
+
expect(resolveActiveOrganizationId({ orgId: null, actorOrgId: ' ' })).toBeNull()
|
|
83
|
+
expect(resolveActiveOrganizationId({ orgId: null, actorOrgId: 42 })).toBeNull()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('still honours an explicit organization selection across tenants', () => {
|
|
87
|
+
expect(
|
|
88
|
+
resolveActiveOrganizationId({
|
|
89
|
+
orgId: selectedOrgId,
|
|
90
|
+
actorOrgId: accountOrgId,
|
|
91
|
+
tenantId: foreignTenantId,
|
|
92
|
+
actorTenantId,
|
|
93
|
+
}),
|
|
94
|
+
).toBe(selectedOrgId)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('organizationScopeRequiredResponse', () => {
|
|
99
|
+
// 401 would send `apiFetch` back into the session-refresh loop; the scope error must not.
|
|
100
|
+
it('answers 400 with a machine-readable code', async () => {
|
|
101
|
+
const response = organizationScopeRequiredResponse()
|
|
102
|
+
expect(response.status).toBe(400)
|
|
103
|
+
const body = await response.json()
|
|
104
|
+
expect(body.code).toBe(ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE)
|
|
105
|
+
expect(typeof body.error).toBe('string')
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
type OrganizationScopedAuth = {
|
|
2
|
+
orgId?: string | null
|
|
3
|
+
actorOrgId?: unknown
|
|
4
|
+
tenantId?: string | null
|
|
5
|
+
actorTenantId?: unknown
|
|
6
|
+
} | null | undefined
|
|
7
|
+
|
|
8
|
+
function normalizeId(value: unknown): string | null {
|
|
9
|
+
if (typeof value !== 'string') return null
|
|
10
|
+
const trimmed = value.trim()
|
|
11
|
+
return trimmed.length > 0 ? trimmed : null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the organization a request is scoped to when the caller may be viewing
|
|
16
|
+
* "all organizations".
|
|
17
|
+
*
|
|
18
|
+
* Organization-scoped configuration modules (integrations credentials/state, data sync
|
|
19
|
+
* mappings/schedules/runs) all require a non-null `organization_id`, so there is no
|
|
20
|
+
* meaningful "all organizations" view of them. When an operator selects that option the
|
|
21
|
+
* super-admin cookie override clears `auth.orgId` and preserves the actor's own
|
|
22
|
+
* organization in `actorOrgId`; fall back to it so those modules keep showing the
|
|
23
|
+
* operator's own configuration instead of failing.
|
|
24
|
+
*
|
|
25
|
+
* The fallback is only valid while the effective tenant is still the actor's own tenant.
|
|
26
|
+
* When the super-admin cookie override also switched tenants (`actorTenantId` is present
|
|
27
|
+
* and differs from `auth.tenantId`), the actor's organization belongs to another tenant —
|
|
28
|
+
* scoping to it would persist a cross-tenant `{ organizationId, tenantId }` pair. Return
|
|
29
|
+
* `null` instead and let the route answer with `organizationScopeRequiredResponse()`.
|
|
30
|
+
*
|
|
31
|
+
* Answering 401 for an unresolvable scope is not merely wrong but self-perpetuating:
|
|
32
|
+
* `apiFetch` reads 401 as an expired session and redirects through
|
|
33
|
+
* `/api/auth/session/refresh`, which succeeds and returns to the same page, reloading
|
|
34
|
+
* forever. That is why the missing-scope answer is a 400, never a 401.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveActiveOrganizationId(auth: OrganizationScopedAuth): string | null {
|
|
37
|
+
if (!auth) return null
|
|
38
|
+
const selected = normalizeId(auth.orgId)
|
|
39
|
+
if (selected) return selected
|
|
40
|
+
const actorOrgId = normalizeId(auth.actorOrgId)
|
|
41
|
+
if (!actorOrgId) return null
|
|
42
|
+
if ('actorTenantId' in auth) {
|
|
43
|
+
const actorTenantId = normalizeId(auth.actorTenantId)
|
|
44
|
+
const effectiveTenantId = normalizeId(auth.tenantId)
|
|
45
|
+
if (!actorTenantId || actorTenantId !== effectiveTenantId) return null
|
|
46
|
+
}
|
|
47
|
+
return actorOrgId
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE = 'organization_scope_required'
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 400 response for an authenticated caller whose organization scope cannot be resolved
|
|
54
|
+
* (e.g. a super-admin viewing a foreign tenant with "all organizations" selected).
|
|
55
|
+
* Deliberately not a 401: the session is valid, so refreshing it would loop.
|
|
56
|
+
*/
|
|
57
|
+
export function organizationScopeRequiredResponse(): Response {
|
|
58
|
+
return Response.json(
|
|
59
|
+
{
|
|
60
|
+
error: 'Select an organization to access this resource',
|
|
61
|
+
code: ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,
|
|
62
|
+
},
|
|
63
|
+
{ status: 400 },
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -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
|
+
})
|