@open-mercato/shared 0.6.7 → 0.6.8-develop.6874.1.982d6097d8

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 (195) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +57 -6
  3. package/build.mjs +2 -4
  4. package/dist/lib/ai/safety-identifier.js +32 -0
  5. package/dist/lib/ai/safety-identifier.js.map +7 -0
  6. package/dist/lib/auth/organizationScope.js +34 -0
  7. package/dist/lib/auth/organizationScope.js.map +7 -0
  8. package/dist/lib/auth/server.js +15 -2
  9. package/dist/lib/auth/server.js.map +2 -2
  10. package/dist/lib/bootstrap/clientOnlyModules.js +55 -0
  11. package/dist/lib/bootstrap/clientOnlyModules.js.map +7 -0
  12. package/dist/lib/bootstrap/dynamicLoader.js +117 -43
  13. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  14. package/dist/lib/commands/command-bus.js +5 -1
  15. package/dist/lib/commands/command-bus.js.map +2 -2
  16. package/dist/lib/commands/command-interceptor-runner.js +2 -2
  17. package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
  18. package/dist/lib/crud/enricher-runner.js +2 -11
  19. package/dist/lib/crud/enricher-runner.js.map +2 -2
  20. package/dist/lib/crud/factory.js +72 -9
  21. package/dist/lib/crud/factory.js.map +2 -2
  22. package/dist/lib/crud/interceptor-runner.js +2 -2
  23. package/dist/lib/crud/interceptor-runner.js.map +2 -2
  24. package/dist/lib/crud/mutation-guard-registry.js +2 -2
  25. package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
  26. package/dist/lib/crud/types.js +4 -0
  27. package/dist/lib/crud/types.js.map +3 -3
  28. package/dist/lib/data/consistency.js +19 -0
  29. package/dist/lib/data/consistency.js.map +7 -0
  30. package/dist/lib/data/engine.js +37 -11
  31. package/dist/lib/data/engine.js.map +2 -2
  32. package/dist/lib/db/entityMetadata.js +26 -0
  33. package/dist/lib/db/entityMetadata.js.map +7 -0
  34. package/dist/lib/db/pg-errors.js +42 -0
  35. package/dist/lib/db/pg-errors.js.map +2 -2
  36. package/dist/lib/di/container.js +48 -6
  37. package/dist/lib/di/container.js.map +2 -2
  38. package/dist/lib/email/send.js +56 -0
  39. package/dist/lib/email/send.js.map +2 -2
  40. package/dist/lib/encryption/subscriber.js +2 -10
  41. package/dist/lib/encryption/subscriber.js.map +2 -2
  42. package/dist/lib/encryption/tenantDataEncryptionService.js +52 -17
  43. package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
  44. package/dist/lib/i18n/config.js +1 -1
  45. package/dist/lib/i18n/config.js.map +2 -2
  46. package/dist/lib/i18n/server.js +4 -4
  47. package/dist/lib/i18n/server.js.map +3 -3
  48. package/dist/lib/logger/extension.js +29 -0
  49. package/dist/lib/logger/extension.js.map +7 -0
  50. package/dist/lib/logger/index.js +47 -3
  51. package/dist/lib/logger/index.js.map +3 -3
  52. package/dist/lib/modules/registry.js +5 -1
  53. package/dist/lib/modules/registry.js.map +2 -2
  54. package/dist/lib/modules/surfaceFingerprint.js +47 -0
  55. package/dist/lib/modules/surfaceFingerprint.js.map +7 -0
  56. package/dist/lib/query/ciphertext-search-warning.js +45 -0
  57. package/dist/lib/query/ciphertext-search-warning.js.map +7 -0
  58. package/dist/lib/query/engine.js +54 -40
  59. package/dist/lib/query/engine.js.map +2 -2
  60. package/dist/lib/query/types.js.map +1 -1
  61. package/dist/lib/redis/connection.js +7 -2
  62. package/dist/lib/redis/connection.js.map +2 -2
  63. package/dist/lib/search/auto-indexing.js +14 -0
  64. package/dist/lib/search/auto-indexing.js.map +7 -0
  65. package/dist/lib/search/availability.js +107 -0
  66. package/dist/lib/search/availability.js.map +7 -0
  67. package/dist/lib/search/config.js +61 -2
  68. package/dist/lib/search/config.js.map +2 -2
  69. package/dist/lib/search/tokenLookup.js +46 -0
  70. package/dist/lib/search/tokenLookup.js.map +7 -0
  71. package/dist/lib/search/tokenize.js +23 -11
  72. package/dist/lib/search/tokenize.js.map +2 -2
  73. package/dist/lib/telemetry/runtime.js +35 -0
  74. package/dist/lib/telemetry/runtime.js.map +7 -0
  75. package/dist/lib/version.js +1 -1
  76. package/dist/lib/version.js.map +1 -1
  77. package/dist/lib/webhooks/body.js +57 -0
  78. package/dist/lib/webhooks/body.js.map +7 -0
  79. package/dist/lib/webhooks/inbound-types.js +1 -0
  80. package/dist/lib/webhooks/inbound-types.js.map +7 -0
  81. package/dist/lib/webhooks/index.js +13 -1
  82. package/dist/lib/webhooks/index.js.map +2 -2
  83. package/dist/lib/webhooks/verify.js +12 -7
  84. package/dist/lib/webhooks/verify.js.map +2 -2
  85. package/dist/modules/analytics.js.map +2 -2
  86. package/dist/modules/overrides.js +50 -1
  87. package/dist/modules/overrides.js.map +2 -2
  88. package/dist/modules/payment_gateways/types.js +8 -1
  89. package/dist/modules/payment_gateways/types.js.map +2 -2
  90. package/dist/modules/registry.js +1 -0
  91. package/dist/modules/registry.js.map +2 -2
  92. package/dist/modules/widgets/extension-points.js +96 -0
  93. package/dist/modules/widgets/extension-points.js.map +7 -0
  94. package/dist/security/enabledModulesRegistry.js +56 -11
  95. package/dist/security/enabledModulesRegistry.js.map +2 -2
  96. package/dist/security/featurePolicy.js +62 -0
  97. package/dist/security/featurePolicy.js.map +7 -0
  98. package/jest.config.cjs +2 -1
  99. package/package.json +19 -9
  100. package/scripts/versionSource.cjs +65 -0
  101. package/src/lib/__tests__/versionSource.test.ts +46 -0
  102. package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
  103. package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
  104. package/src/lib/ai/llm-provider.ts +33 -0
  105. package/src/lib/ai/safety-identifier.ts +73 -0
  106. package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
  107. package/src/lib/auth/__tests__/server.test.ts +23 -0
  108. package/src/lib/auth/organizationScope.ts +65 -0
  109. package/src/lib/auth/server.ts +23 -2
  110. package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +189 -0
  111. package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
  112. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
  113. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +63 -8
  114. package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +256 -0
  115. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +21 -1
  116. package/src/lib/bootstrap/clientOnlyModules.ts +85 -0
  117. package/src/lib/bootstrap/dynamicLoader.ts +213 -65
  118. package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +28 -0
  119. package/src/lib/commands/command-bus.ts +5 -1
  120. package/src/lib/commands/command-interceptor-runner.ts +2 -2
  121. package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +171 -0
  122. package/src/lib/crud/__tests__/crud-factory.test.ts +351 -0
  123. package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
  124. package/src/lib/crud/enricher-runner.ts +2 -11
  125. package/src/lib/crud/factory.ts +82 -6
  126. package/src/lib/crud/interceptor-runner.ts +2 -2
  127. package/src/lib/crud/mutation-guard-registry.ts +2 -2
  128. package/src/lib/crud/types.ts +3 -0
  129. package/src/lib/data/__tests__/consistency.test.ts +38 -0
  130. package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
  131. package/src/lib/data/consistency.ts +17 -0
  132. package/src/lib/data/engine.ts +50 -16
  133. package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
  134. package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
  135. package/src/lib/db/entityMetadata.ts +43 -0
  136. package/src/lib/db/pg-errors.ts +58 -0
  137. package/src/lib/di/__tests__/container-app-di-absent.test.ts +155 -0
  138. package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
  139. package/src/lib/di/__tests__/registrar-error-log.test.ts +154 -0
  140. package/src/lib/di/container.ts +72 -8
  141. package/src/lib/email/__tests__/send.test.ts +37 -1
  142. package/src/lib/email/send.ts +79 -0
  143. package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
  144. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
  145. package/src/lib/encryption/subscriber.ts +2 -14
  146. package/src/lib/encryption/tenantDataEncryptionService.ts +64 -18
  147. package/src/lib/i18n/__tests__/config.test.ts +8 -0
  148. package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
  149. package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
  150. package/src/lib/i18n/config.ts +2 -2
  151. package/src/lib/i18n/server.ts +6 -2
  152. package/src/lib/logger/__tests__/logger.test.ts +77 -0
  153. package/src/lib/logger/extension.ts +59 -0
  154. package/src/lib/logger/index.ts +59 -1
  155. package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +122 -0
  156. package/src/lib/modules/registry.ts +10 -0
  157. package/src/lib/modules/surfaceFingerprint.ts +87 -0
  158. package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +178 -0
  159. package/src/lib/query/__tests__/engine.test.ts +65 -13
  160. package/src/lib/query/ciphertext-search-warning.ts +95 -0
  161. package/src/lib/query/engine.ts +75 -58
  162. package/src/lib/query/types.ts +4 -5
  163. package/src/lib/redis/__tests__/connection.test.ts +16 -0
  164. package/src/lib/redis/connection.ts +10 -7
  165. package/src/lib/search/__tests__/availability.test.ts +211 -0
  166. package/src/lib/search/__tests__/config.test.ts +177 -0
  167. package/src/lib/search/__tests__/tokenLookup.test.ts +206 -0
  168. package/src/lib/search/__tests__/tokenize.test.ts +49 -0
  169. package/src/lib/search/auto-indexing.ts +22 -0
  170. package/src/lib/search/availability.ts +232 -0
  171. package/src/lib/search/config.ts +106 -8
  172. package/src/lib/search/tokenLookup.ts +133 -0
  173. package/src/lib/search/tokenize.ts +33 -11
  174. package/src/lib/telemetry/runtime.ts +72 -0
  175. package/src/lib/webhooks/__tests__/body.test.ts +95 -0
  176. package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
  177. package/src/lib/webhooks/body.ts +70 -0
  178. package/src/lib/webhooks/inbound-types.ts +125 -0
  179. package/src/lib/webhooks/index.ts +19 -1
  180. package/src/lib/webhooks/verify.ts +17 -9
  181. package/src/modules/__tests__/nav-group-order-override.test.ts +183 -0
  182. package/src/modules/__tests__/registry.test.ts +10 -2
  183. package/src/modules/analytics.ts +7 -0
  184. package/src/modules/customer-auth.ts +1 -0
  185. package/src/modules/encryption.ts +3 -0
  186. package/src/modules/navigation/backendChrome.ts +15 -0
  187. package/src/modules/overrides.ts +103 -0
  188. package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
  189. package/src/modules/payment_gateways/types.ts +41 -0
  190. package/src/modules/registry.ts +3 -0
  191. package/src/modules/widgets/__tests__/extension-points.test.ts +101 -0
  192. package/src/modules/widgets/extension-points.ts +524 -0
  193. package/src/security/__tests__/featurePolicy.test.ts +166 -0
  194. package/src/security/enabledModulesRegistry.ts +64 -12
  195. package/src/security/featurePolicy.ts +89 -0
@@ -116,6 +116,21 @@ export interface EncryptionOverridesShape {
116
116
  maps?: EncryptionMapOverridesMap | LooseOverrideMap
117
117
  }
118
118
 
119
+ /**
120
+ * Backend navigation ordering.
121
+ *
122
+ * `groupOrder` **prepends** sidebar nav group ids: the ids listed here rank ahead of every other
123
+ * group, in the order given, and any group not named keeps the ordering it has today. Prepending
124
+ * rather than replacing means an app that only cares about its own group lists that one id, instead of
125
+ * having to enumerate every shipped group and accidentally demoting the ones it forgot.
126
+ *
127
+ * This is a *default*, applied beneath both role and user sidebar preferences — an operator's own
128
+ * arrangement still wins.
129
+ */
130
+ export interface NavOverridesShape {
131
+ groupOrder?: string[]
132
+ }
133
+
119
134
  /**
120
135
  * Umbrella shape for `entry.overrides`. Every key is optional; a
121
136
  * downstream app sets only the domains it cares about.
@@ -136,6 +151,7 @@ export interface ModuleOverrides {
136
151
  acl?: AclOverridesShape
137
152
  di?: DiOverridesMap | LooseOverrideMap
138
153
  encryption?: EncryptionOverridesShape
154
+ nav?: NavOverridesShape
139
155
  }
140
156
 
141
157
  /**
@@ -175,6 +191,7 @@ export type ModuleOverrideDomain =
175
191
  | 'acl'
176
192
  | 'di'
177
193
  | 'encryption'
194
+ | 'nav'
178
195
 
179
196
  export interface ModuleOverrideEntry<TShape> {
180
197
  moduleId: string
@@ -227,6 +244,7 @@ const DOMAIN_KEYS: ModuleOverrideDomain[] = [
227
244
  'acl',
228
245
  'di',
229
246
  'encryption',
247
+ 'nav',
230
248
  ]
231
249
 
232
250
  const TRACKING_ISSUE_HINT =
@@ -441,6 +459,71 @@ const encryptionMapOverrideStore: OverrideStore<ModuleEncryptionMap> = { modules
441
459
  const diOverrideStore: OverrideStore<Exclude<DiBindingOverride, null>> = { modules: {}, programmatic: {} }
442
460
  const setupOverridesByModule: Record<string, SetupOverridesShape> = {}
443
461
 
462
+ /**
463
+ * Sidebar nav ordering state.
464
+ *
465
+ * Persisted on `globalThis` rather than in a module-local variable. This is the one override domain
466
+ * whose consumer lives in a *different package* (`@open-mercato/core`'s backend chrome reads what the
467
+ * app's bootstrap wrote), and standalone builds can evaluate `@open-mercato/shared` through more than
468
+ * one server chunk — bootstrap would store the value in one instance while the reader saw `null` from
469
+ * another. See `.ai/lessons.md`, "Global registries in publishable packages must use `globalThis`".
470
+ *
471
+ * Two tiers, matching every other override domain and the documented resolution order: programmatic
472
+ * calls win over `modules.ts` inline declarations.
473
+ */
474
+ const GLOBAL_NAV_OVERRIDE_STATE_KEY = '__openMercatoNavOverrideState__'
475
+
476
+ type NavOverrideState = {
477
+ /** From `modules.ts` inline `overrides.nav`, with the module entry that supplied it. */
478
+ modules: { moduleId: string; groupOrder: string[] } | null
479
+ /** From `applyNavGroupOrderOverrides`. Takes precedence over `modules`. */
480
+ programmatic: string[] | null
481
+ }
482
+
483
+ function getNavOverrideState(): NavOverrideState {
484
+ const existing = (globalThis as Record<string, unknown>)[GLOBAL_NAV_OVERRIDE_STATE_KEY]
485
+ if (existing && typeof existing === 'object') {
486
+ const typed = existing as NavOverrideState
487
+ if ('modules' in typed && 'programmatic' in typed) return typed
488
+ }
489
+ const initial: NavOverrideState = { modules: null, programmatic: null }
490
+ ;(globalThis as Record<string, unknown>)[GLOBAL_NAV_OVERRIDE_STATE_KEY] = initial
491
+ return initial
492
+ }
493
+
494
+ /** Drops blank/duplicate ids and returns `null` when nothing usable remains. */
495
+ function normalizeNavGroupOrder(value: unknown): string[] | null {
496
+ if (!Array.isArray(value)) return null
497
+ const ids = Array.from(
498
+ new Set(
499
+ value
500
+ .filter((id): id is string => typeof id === 'string' && id.trim().length > 0)
501
+ .map((id) => id.trim()),
502
+ ),
503
+ )
504
+ return ids.length > 0 ? ids : null
505
+ }
506
+
507
+ /**
508
+ * Programmatic nav ordering, for env-driven boot decisions and tests. Takes precedence over
509
+ * `modules.ts` inline `overrides.nav`, consistent with the other domains' programmatic tier. Pass
510
+ * `null` to clear it and fall back to the inline declaration.
511
+ */
512
+ export function applyNavGroupOrderOverrides(groupOrder: string[] | null): void {
513
+ getNavOverrideState().programmatic = groupOrder === null ? null : normalizeNavGroupOrder(groupOrder)
514
+ }
515
+
516
+ /**
517
+ * Nav group ids an app wants ranked ahead of the built-in order, or `null` when none is configured.
518
+ *
519
+ * Consumers MUST treat `null` as "use the shipped ordering unchanged" — this is a default, applied
520
+ * beneath role and user sidebar preferences.
521
+ */
522
+ export function getNavGroupOrderOverride(): readonly string[] | null {
523
+ const state = getNavOverrideState()
524
+ return state.programmatic ?? state.modules?.groupOrder ?? null
525
+ }
526
+
444
527
  function normalizeIdOverrideKey(key: string, label: string): string | null {
445
528
  if (typeof key !== 'string') return null
446
529
  const trimmed = key.trim()
@@ -721,6 +804,9 @@ export function resetModuleContractOverridesForTests(): void {
721
804
  clearStore(encryptionMapOverrideStore)
722
805
  clearStore(diOverrideStore)
723
806
  for (const key of Object.keys(setupOverridesByModule)) delete setupOverridesByModule[key]
807
+ const navState = getNavOverrideState()
808
+ navState.modules = null
809
+ navState.programmatic = null
724
810
  }
725
811
 
726
812
  /**
@@ -1500,7 +1586,24 @@ function encryptionOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<E
1500
1586
  }
1501
1587
  }
1502
1588
 
1589
+ function navOverridesApplier(entries: ReadonlyArray<ModuleOverrideEntry<NavOverridesShape>>): void {
1590
+ const state = getNavOverrideState()
1591
+ for (const entry of entries) {
1592
+ const groupOrder = normalizeNavGroupOrder(entry.overrides?.groupOrder)
1593
+ if (!groupOrder) continue
1594
+ if (state.modules && state.modules.moduleId !== entry.moduleId) {
1595
+ logger.warn('nav.groupOrder declared by more than one module — the later one wins', {
1596
+ previousModuleId: state.modules.moduleId,
1597
+ moduleId: entry.moduleId,
1598
+ hint: 'Sidebar group ordering is a single app-wide decision; declare it on one module entry.',
1599
+ })
1600
+ }
1601
+ state.modules = { moduleId: entry.moduleId, groupOrder }
1602
+ }
1603
+ }
1604
+
1503
1605
  function registerBuiltInModuleOverrideAppliers(): void {
1606
+ registerModuleOverrideApplier<NavOverridesShape>('nav', navOverridesApplier)
1504
1607
  registerModuleOverrideApplier<RoutesOverridesShape>('routes', routesOverridesApplier)
1505
1608
  registerModuleOverrideApplier<EventsOverridesShape>('events', eventsOverridesApplier)
1506
1609
  registerModuleOverrideApplier<WorkerOverridesMap>('workers', workersOverridesApplier)
@@ -0,0 +1,37 @@
1
+ import {
2
+ clearWebhookHandlers,
3
+ getWebhookHandler,
4
+ registerWebhookHandler,
5
+ type VerifyWebhookInput,
6
+ type WebhookEvent,
7
+ } from '../types'
8
+
9
+ const handler = async (_input: VerifyWebhookInput): Promise<WebhookEvent> => ({
10
+ eventType: 'payment.updated',
11
+ eventId: 'evt-1',
12
+ data: {},
13
+ idempotencyKey: 'evt-1',
14
+ timestamp: new Date(0),
15
+ })
16
+
17
+ describe('payment gateway webhook registration', () => {
18
+ beforeEach(() => {
19
+ clearWebhookHandlers()
20
+ })
21
+
22
+ it('preserves an optional valid body limit on the registration', () => {
23
+ registerWebhookHandler('limited', handler, { maxBodyBytes: 64 * 1024 })
24
+
25
+ expect(getWebhookHandler('limited')?.maxBodyBytes).toBe(64 * 1024)
26
+ })
27
+
28
+ it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER])(
29
+ 'rejects an invalid body limit: %s',
30
+ (maxBodyBytes) => {
31
+ expect(() => registerWebhookHandler('invalid', handler, { maxBodyBytes })).toThrow(
32
+ '[internal] Payment gateway webhook maxBodyBytes must be a positive safe integer',
33
+ )
34
+ expect(getWebhookHandler('invalid')).toBeUndefined()
35
+ },
36
+ )
37
+ })
@@ -1,3 +1,5 @@
1
+ import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES } from '../../lib/webhooks/body'
2
+
1
3
  // ── Unified Payment Status ──────────────────────────────────────────────────
2
4
 
3
5
  export type UnifiedPaymentStatus =
@@ -112,6 +114,32 @@ export type PaymentGatewayClientSession =
112
114
  | EmbeddedPaymentGatewayClientSession
113
115
  | RedirectPaymentGatewayClientSession
114
116
 
117
+ // ── Order Amount Reconciliation ─────────────────────────────────────────────
118
+
119
+ export interface PaymentGatewayScope {
120
+ organizationId: string
121
+ tenantId: string
122
+ }
123
+
124
+ export interface PaymentOrderTotal {
125
+ orderId: string
126
+ currencyCode: string
127
+ amountDue: number
128
+ }
129
+
130
+ /**
131
+ * Contract a module that owns orders implements so `payment_gateways` can
132
+ * reconcile a caller-supplied session amount against the authoritative amount
133
+ * due, without depending on that module. Implementations MUST scope the lookup
134
+ * by tenant and organization and MUST return `null` for anything outside the
135
+ * caller's scope, so an out-of-scope order is indistinguishable from a missing
136
+ * one. The `sales` module registers the default implementation under the
137
+ * `paymentOrderTotalResolver` DI name.
138
+ */
139
+ export interface PaymentOrderTotalResolver {
140
+ resolveOrderTotal(orderId: string, scope: PaymentGatewayScope): Promise<PaymentOrderTotal | null>
141
+ }
142
+
115
143
  // ── Input / Output Types ────────────────────────────────────────────────────
116
144
 
117
145
  export interface CreateSessionInput {
@@ -225,6 +253,7 @@ export interface WebhookHandlerRegistration {
225
253
  handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>
226
254
  queue?: string
227
255
  readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null
256
+ maxBodyBytes?: number
228
257
  }
229
258
 
230
259
  // ── Adapter Registry Options ────────────────────────────────────────────────
@@ -319,13 +348,25 @@ export function registerWebhookHandler(
319
348
  options?: {
320
349
  queue?: string
321
350
  readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null
351
+ maxBodyBytes?: number
322
352
  },
323
353
  ): () => void {
354
+ if (
355
+ options?.maxBodyBytes !== undefined
356
+ && (!Number.isSafeInteger(options.maxBodyBytes)
357
+ || options.maxBodyBytes <= 0
358
+ || options.maxBodyBytes > DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)
359
+ ) {
360
+ throw new Error(
361
+ `[internal] Payment gateway webhook maxBodyBytes must be a positive safe integer no greater than ${DEFAULT_WEBHOOK_BODY_LIMIT_BYTES}`,
362
+ )
363
+ }
324
364
  const webhookHandlerRegistry = getWebhookHandlerRegistry()
325
365
  webhookHandlerRegistry.set(providerKey, {
326
366
  handler,
327
367
  queue: options?.queue,
328
368
  readSessionIdHint: options?.readSessionIdHint,
369
+ maxBodyBytes: options?.maxBodyBytes,
329
370
  })
330
371
  return () => {
331
372
  webhookHandlerRegistry.delete(providerKey)
@@ -194,6 +194,8 @@ export type ModuleWorker = {
194
194
  moduleId?: string
195
195
  queue: string
196
196
  concurrency: number
197
+ lockDuration?: number
198
+ maxStalledCount?: number
197
199
  handler: ModuleWorkerHandler
198
200
  }
199
201
 
@@ -513,6 +515,7 @@ export function getDefaultEncryptionMaps(modules: Module[]): import('./encryptio
513
515
  moduleId: mod.id,
514
516
  map: {
515
517
  entityId: entry.entityId,
518
+ ...(entry.keyScope ? { keyScope: entry.keyScope } : {}),
516
519
  fields: entry.fields.map((field) => ({
517
520
  field: field.field,
518
521
  hashField: field.hashField ?? null,
@@ -0,0 +1,101 @@
1
+ import {
2
+ CRUD_FORM_EXTENSION_SURFACES,
3
+ CRUD_FORM_LIFECYCLE_PHASES,
4
+ CRUD_FORM_OPERATIONS,
5
+ DATA_TABLE_EXTENSION_SURFACES,
6
+ crudFormExtensionHost,
7
+ crudFormExtensionSpotId,
8
+ dataTableExtensionHost,
9
+ dataTableExtensionSpotId,
10
+ defineModuleExtensionPoints,
11
+ extensionSpotChildId,
12
+ injectionExtensionHost,
13
+ resolveExtensionPointPattern,
14
+ } from '@open-mercato/shared/modules/widgets/extension-points'
15
+
16
+ describe('module extension point declarations', () => {
17
+ it('preserves exact ids and immutable declarations', () => {
18
+ const extensionPoints = defineModuleExtensionPoints({
19
+ moduleId: 'catalog',
20
+ hosts: {
21
+ products: dataTableExtensionHost({
22
+ baseSpotId: 'data-table:catalog.products',
23
+ tableId: 'catalog.products.list',
24
+ source: 'components/products/ProductsDataTable.tsx',
25
+ }),
26
+ productForm: crudFormExtensionHost({
27
+ entityId: 'catalog.product',
28
+ spotId: 'crud-form:catalog.product',
29
+ source: 'backend/catalog/products/[id]/page.tsx',
30
+ }),
31
+ },
32
+ })
33
+
34
+ expect(extensionPoints.moduleId).toBe('catalog')
35
+ expect(extensionPoints.hosts.products.tableId).toBe('catalog.products.list')
36
+ expect(Object.isFrozen(extensionPoints)).toBe(true)
37
+ expect(Object.isFrozen(extensionPoints.hosts)).toBe(true)
38
+ })
39
+
40
+ it('requires named parameters for dynamic patterns', () => {
41
+ // @ts-expect-error patterned declarations require named parameters
42
+ expect(() => injectionExtensionHost({
43
+ family: 'integration',
44
+ pattern: 'integrations.detail:{integrationId}',
45
+ supported: ['render-widget'],
46
+ source: 'backend/integrations/[id]/page.tsx',
47
+ })).toThrow('patterned injection extension hosts require named parameters')
48
+ })
49
+
50
+ it('describes only the surfaces bound by DataTable and CrudForm', () => {
51
+ expect(DATA_TABLE_EXTENSION_SURFACES.filter((surface) => surface.bound).map((surface) => surface.key)).toEqual([
52
+ 'header',
53
+ 'footer',
54
+ 'toolbar',
55
+ 'searchTrailing',
56
+ 'columns',
57
+ 'rowActions',
58
+ 'bulkActions',
59
+ 'filters',
60
+ 'replacement',
61
+ ])
62
+ expect(DATA_TABLE_EXTENSION_SURFACES.find((surface) => surface.key === 'emptyState')?.bound).toBe(false)
63
+ expect(CRUD_FORM_EXTENSION_SURFACES.filter((surface) => !surface.bound).map((surface) => surface.key)).toEqual([
64
+ 'beforeFields',
65
+ 'afterFields',
66
+ 'footer',
67
+ 'sidebar',
68
+ 'group',
69
+ 'fieldBefore',
70
+ 'fieldAfter',
71
+ ])
72
+ expect(CRUD_FORM_LIFECYCLE_PHASES).toEqual([
73
+ 'transformValidation',
74
+ 'transformDisplayData',
75
+ 'onBeforeNavigate',
76
+ 'onAppEvent',
77
+ 'onVisibilityChange',
78
+ 'onBeforeDelete',
79
+ 'onDelete',
80
+ 'onAfterDelete',
81
+ 'onDeleteError',
82
+ 'onFieldChange',
83
+ 'transformFormData',
84
+ 'onBeforeSave',
85
+ 'onSave',
86
+ 'onAfterSave',
87
+ ])
88
+ expect(CRUD_FORM_OPERATIONS).toEqual(['create', 'update', 'delete'])
89
+ })
90
+
91
+ it('builds standard family ids without changing punctuation', () => {
92
+ expect(dataTableExtensionSpotId('catalog.products.list', 'columns')).toBe(
93
+ 'data-table:catalog.products.list:columns',
94
+ )
95
+ expect(crudFormExtensionSpotId('catalog.product', 'fields')).toBe('crud-form:catalog.product:fields')
96
+ expect(extensionSpotChildId('legacy:custom-host', 'header')).toBe('legacy:custom-host:header')
97
+ expect(resolveExtensionPointPattern('integrations.detail:{integrationId}', {
98
+ integrationId: 'stripe',
99
+ })).toBe('integrations.detail:stripe')
100
+ })
101
+ })