@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +57 -6
- package/build.mjs +2 -4
- package/dist/lib/ai/safety-identifier.js +32 -0
- package/dist/lib/ai/safety-identifier.js.map +7 -0
- package/dist/lib/auth/organizationScope.js +34 -0
- package/dist/lib/auth/organizationScope.js.map +7 -0
- package/dist/lib/auth/server.js +15 -2
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/clientOnlyModules.js +55 -0
- package/dist/lib/bootstrap/clientOnlyModules.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +117 -43
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +5 -1
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +2 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +2 -11
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +72 -9
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-runner.js +2 -2
- package/dist/lib/crud/interceptor-runner.js.map +2 -2
- package/dist/lib/crud/mutation-guard-registry.js +2 -2
- package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
- package/dist/lib/crud/types.js +4 -0
- package/dist/lib/crud/types.js.map +3 -3
- package/dist/lib/data/consistency.js +19 -0
- package/dist/lib/data/consistency.js.map +7 -0
- package/dist/lib/data/engine.js +37 -11
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/entityMetadata.js +26 -0
- package/dist/lib/db/entityMetadata.js.map +7 -0
- package/dist/lib/db/pg-errors.js +42 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/di/container.js +48 -6
- 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/encryption/subscriber.js +2 -10
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +52 -17
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/i18n/config.js +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/extension.js +29 -0
- package/dist/lib/logger/extension.js.map +7 -0
- package/dist/lib/logger/index.js +47 -3
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +5 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/modules/surfaceFingerprint.js +47 -0
- package/dist/lib/modules/surfaceFingerprint.js.map +7 -0
- package/dist/lib/query/ciphertext-search-warning.js +45 -0
- package/dist/lib/query/ciphertext-search-warning.js.map +7 -0
- package/dist/lib/query/engine.js +54 -40
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/redis/connection.js +7 -2
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/search/auto-indexing.js +14 -0
- package/dist/lib/search/auto-indexing.js.map +7 -0
- package/dist/lib/search/availability.js +107 -0
- package/dist/lib/search/availability.js.map +7 -0
- package/dist/lib/search/config.js +61 -2
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenLookup.js +46 -0
- package/dist/lib/search/tokenLookup.js.map +7 -0
- package/dist/lib/search/tokenize.js +23 -11
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/telemetry/runtime.js +35 -0
- package/dist/lib/telemetry/runtime.js.map +7 -0
- 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/inbound-types.js +1 -0
- package/dist/lib/webhooks/inbound-types.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/analytics.js.map +2 -2
- package/dist/modules/overrides.js +50 -1
- package/dist/modules/overrides.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/registry.js +1 -0
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/extension-points.js +96 -0
- package/dist/modules/widgets/extension-points.js.map +7 -0
- package/dist/security/enabledModulesRegistry.js +56 -11
- package/dist/security/enabledModulesRegistry.js.map +2 -2
- package/dist/security/featurePolicy.js +62 -0
- package/dist/security/featurePolicy.js.map +7 -0
- package/jest.config.cjs +2 -1
- package/package.json +19 -9
- package/scripts/versionSource.cjs +65 -0
- package/src/lib/__tests__/versionSource.test.ts +46 -0
- package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
- package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
- package/src/lib/ai/llm-provider.ts +33 -0
- package/src/lib/ai/safety-identifier.ts +73 -0
- package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
- package/src/lib/auth/__tests__/server.test.ts +23 -0
- package/src/lib/auth/organizationScope.ts +65 -0
- package/src/lib/auth/server.ts +23 -2
- package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +189 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +63 -8
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +256 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +21 -1
- package/src/lib/bootstrap/clientOnlyModules.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +213 -65
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +28 -0
- package/src/lib/commands/command-bus.ts +5 -1
- package/src/lib/commands/command-interceptor-runner.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +171 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +351 -0
- package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
- package/src/lib/crud/enricher-runner.ts +2 -11
- package/src/lib/crud/factory.ts +82 -6
- package/src/lib/crud/interceptor-runner.ts +2 -2
- package/src/lib/crud/mutation-guard-registry.ts +2 -2
- package/src/lib/crud/types.ts +3 -0
- package/src/lib/data/__tests__/consistency.test.ts +38 -0
- package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
- package/src/lib/data/consistency.ts +17 -0
- package/src/lib/data/engine.ts +50 -16
- package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
- package/src/lib/db/entityMetadata.ts +43 -0
- package/src/lib/db/pg-errors.ts +58 -0
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +155 -0
- package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
- package/src/lib/di/__tests__/registrar-error-log.test.ts +154 -0
- package/src/lib/di/container.ts +72 -8
- package/src/lib/email/__tests__/send.test.ts +37 -1
- package/src/lib/email/send.ts +79 -0
- package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
- package/src/lib/encryption/subscriber.ts +2 -14
- package/src/lib/encryption/tenantDataEncryptionService.ts +64 -18
- package/src/lib/i18n/__tests__/config.test.ts +8 -0
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +6 -2
- package/src/lib/logger/__tests__/logger.test.ts +77 -0
- package/src/lib/logger/extension.ts +59 -0
- package/src/lib/logger/index.ts +59 -1
- package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +122 -0
- package/src/lib/modules/registry.ts +10 -0
- package/src/lib/modules/surfaceFingerprint.ts +87 -0
- package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +178 -0
- package/src/lib/query/__tests__/engine.test.ts +65 -13
- package/src/lib/query/ciphertext-search-warning.ts +95 -0
- package/src/lib/query/engine.ts +75 -58
- package/src/lib/query/types.ts +4 -5
- package/src/lib/redis/__tests__/connection.test.ts +16 -0
- package/src/lib/redis/connection.ts +10 -7
- package/src/lib/search/__tests__/availability.test.ts +211 -0
- package/src/lib/search/__tests__/config.test.ts +177 -0
- package/src/lib/search/__tests__/tokenLookup.test.ts +206 -0
- package/src/lib/search/__tests__/tokenize.test.ts +49 -0
- package/src/lib/search/auto-indexing.ts +22 -0
- package/src/lib/search/availability.ts +232 -0
- package/src/lib/search/config.ts +106 -8
- package/src/lib/search/tokenLookup.ts +133 -0
- package/src/lib/search/tokenize.ts +33 -11
- package/src/lib/telemetry/runtime.ts +72 -0
- 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/inbound-types.ts +125 -0
- package/src/lib/webhooks/index.ts +19 -1
- package/src/lib/webhooks/verify.ts +17 -9
- package/src/modules/__tests__/nav-group-order-override.test.ts +183 -0
- package/src/modules/__tests__/registry.test.ts +10 -2
- package/src/modules/analytics.ts +7 -0
- package/src/modules/customer-auth.ts +1 -0
- package/src/modules/encryption.ts +3 -0
- package/src/modules/navigation/backendChrome.ts +15 -0
- package/src/modules/overrides.ts +103 -0
- package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
- package/src/modules/payment_gateways/types.ts +41 -0
- package/src/modules/registry.ts +3 -0
- package/src/modules/widgets/__tests__/extension-points.test.ts +101 -0
- package/src/modules/widgets/extension-points.ts +524 -0
- package/src/security/__tests__/featurePolicy.test.ts +166 -0
- package/src/security/enabledModulesRegistry.ts +64 -12
- package/src/security/featurePolicy.ts +89 -0
package/src/modules/overrides.ts
CHANGED
|
@@ -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)
|
package/src/modules/registry.ts
CHANGED
|
@@ -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
|
+
})
|