@open-mercato/shared 0.6.7-develop.6630.1.c8a615e536 → 0.6.7-develop.6645.1.52f0b37813

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.
@@ -1,2 +1,2 @@
1
- [build:shared] found 239 entry points
1
+ [build:shared] found 240 entry points
2
2
  [build:shared] built successfully
@@ -1,5 +1,5 @@
1
1
  import { registerOrmEntities } from "../db/mikro.js";
2
- import { registerDiRegistrars } from "../di/container.js";
2
+ import { registerAppDiRegistrar, registerDiRegistrars } from "../di/container.js";
3
3
  import { registerModules } from "../modules/registry.js";
4
4
  import { registerEntityIds } from "../encryption/entityIds.js";
5
5
  import { registerEntityFields } from "../encryption/entityFields.js";
@@ -19,6 +19,9 @@ let _bootstrapped = false;
19
19
  let _asyncRegistrationPromise = null;
20
20
  function createBootstrap(data, options = {}) {
21
21
  return function bootstrap() {
22
+ if (options.appDiRegistrar) {
23
+ registerAppDiRegistrar(options.appDiRegistrar);
24
+ }
22
25
  if (_bootstrapped && process.env.NODE_ENV !== "development") return;
23
26
  _bootstrapped = true;
24
27
  registerOrmEntities(data.entities);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/bootstrap/factory.ts"],
4
- "sourcesContent": ["import type { BootstrapData, BootstrapOptions } from './types'\nimport { registerOrmEntities } from '../db/mikro'\nimport { registerDiRegistrars } from '../di/container'\nimport { registerModules } from '../modules/registry'\nimport { registerEntityIds } from '../encryption/entityIds'\nimport { registerEntityFields } from '../encryption/entityFields'\nimport { registerSearchModuleConfigs } from '../../modules/search'\nimport { registerAnalyticsModuleConfigs } from '../../modules/analytics'\nimport { registerCodeWorkflowEntries } from '../../modules/workflows/code-registry'\nimport { registerResponseEnrichers } from '../crud/enricher-registry'\nimport { registerApiInterceptors } from '../crud/interceptor-registry'\nimport { registerComponentOverrides } from '../../modules/widgets/component-registry'\nimport { registerMutationGuards } from '../crud/mutation-guard-store'\nimport { registerCommandInterceptors } from '../commands/command-interceptor-store'\nimport { registerCommandLoaders } from '../commands/registry'\nimport { registerNotificationHandlers } from '../notifications/handler-registry'\nimport { clearRegisteredIntegrations, registerBundles, registerIntegrations } from '../../modules/integrations/types'\nimport { applyComponentOverridesToEntries } from '../../modules/overrides'\n\nlet _bootstrapped = false\n\n// Store the async registration promise so callers can await it if needed\nlet _asyncRegistrationPromise: Promise<void> | null = null\n\n/**\n * Creates a bootstrap function that registers all application dependencies.\n *\n * The returned function should be called once at application startup.\n * In development mode, it can be called multiple times (for HMR).\n *\n * @param data - All generated registry data from .mercato/generated/\n * @param options - Optional configuration\n * @returns A bootstrap function to call at app startup\n */\nexport function createBootstrap(data: BootstrapData, options: BootstrapOptions = {}) {\n return function bootstrap(): void {\n // In development, always re-run registrations to handle HMR\n // (Module state may be reset when Turbopack reloads packages)\n if (_bootstrapped && process.env.NODE_ENV !== 'development') return\n _bootstrapped = true\n\n // === 1. Foundation: ORM entities and DI registrars ===\n registerOrmEntities(data.entities)\n registerDiRegistrars(data.diRegistrars.filter((r): r is NonNullable<typeof r> => r != null))\n\n // === 2. Modules registry (required by i18n, query engine, dashboards, CLI) ===\n registerModules(data.modules)\n clearRegisteredIntegrations()\n for (const module of data.modules) {\n if (module.integrations?.length) {\n registerIntegrations(module.integrations)\n }\n if (module.bundles?.length) {\n registerBundles(module.bundles)\n }\n }\n\n // === 3. Entity IDs (required by encryption, indexing, entity links) ===\n registerEntityIds(data.entityIds)\n\n // === 4. Entity fields registry (for encryption manager, Turbopack compatibility) ===\n if (data.entityFieldsRegistry) {\n registerEntityFields(data.entityFieldsRegistry)\n }\n\n // === 5. Search module configs (for search service registration in DI) ===\n if (data.searchModuleConfigs) {\n registerSearchModuleConfigs(data.searchModuleConfigs)\n }\n\n // === 6. Analytics module configs (for dashboard widgets and analytics API) ===\n if (data.analyticsModuleConfigs) {\n registerAnalyticsModuleConfigs(data.analyticsModuleConfigs)\n }\n\n // === 6a. Code workflow definitions (so CLI/worker processes resolve them like the app runtime) ===\n if (data.codeWorkflows?.length) {\n registerCodeWorkflowEntries(data.codeWorkflows)\n }\n\n // === 6b. Response enrichers (for CRUD response enrichment) ===\n if (data.enricherEntries) {\n registerResponseEnrichers(data.enricherEntries)\n }\n\n // === 6c. API interceptors (for CRUD route interception) ===\n if (data.interceptorEntries) {\n registerApiInterceptors(data.interceptorEntries)\n }\n\n // === 6d. Component overrides (for page/component replacement) ===\n if (data.componentOverrideEntries) {\n const finalEntries = applyComponentOverridesToEntries(data.componentOverrideEntries)\n const allOverrides = finalEntries.flatMap((entry) => entry.componentOverrides ?? [])\n registerComponentOverrides(allOverrides)\n }\n\n // === 6e. Mutation guards (for CRUD mutation lifecycle) ===\n if (data.guardEntries) {\n registerMutationGuards(data.guardEntries)\n }\n\n // === 6f. Command interceptors (for command bus lifecycle) ===\n if (data.commandInterceptorEntries) {\n registerCommandInterceptors(data.commandInterceptorEntries)\n }\n\n // === 6f.1. Command loaders (for lazy command handler registration) ===\n if (data.commandLoaderEntries) {\n registerCommandLoaders(data.commandLoaderEntries)\n }\n\n // === 6g. Notification handlers (reactive notification side-effects) ===\n if (data.notificationHandlerEntries) {\n registerNotificationHandlers(data.notificationHandlerEntries)\n }\n\n // === 7-8. UI Widgets and Optional packages (async to avoid circular deps) ===\n // Store the promise so CLI context can await it\n _asyncRegistrationPromise = registerWidgetsAndOptionalPackages(data, options)\n void _asyncRegistrationPromise\n\n options.onRegistrationComplete?.()\n }\n}\n\n/**\n * Wait for async registrations (CLI modules, widgets, etc.) to complete.\n * Call this after bootstrap() in CLI context where you need modules immediately.\n */\nexport async function waitForAsyncRegistration(): Promise<void> {\n if (_asyncRegistrationPromise) {\n await _asyncRegistrationPromise\n }\n}\n\nasync function registerWidgetsAndOptionalPackages(data: BootstrapData, options: BootstrapOptions): Promise<void> {\n // Register UI widgets (dynamic imports to avoid circular deps with ui/core packages)\n try {\n const [dashboardRegistry, injectionRegistry, coreInjection] = await Promise.all([\n import('@open-mercato/ui/backend/dashboard/widgetRegistry'),\n import('@open-mercato/ui/backend/injection/widgetRegistry'),\n import('@open-mercato/core/modules/widgets/lib/injection'),\n ])\n\n dashboardRegistry.registerDashboardWidgets(data.dashboardWidgetEntries)\n injectionRegistry.registerInjectionWidgets(data.injectionWidgetEntries)\n coreInjection.registerCoreInjectionWidgets(data.injectionWidgetEntries)\n coreInjection.registerCoreInjectionTables(data.injectionTables)\n coreInjection.registerEnabledModuleIds(\n data.modules.map((module) => module.id).filter((id): id is string => typeof id === 'string' && id.length > 0),\n )\n } catch {\n // UI packages may not be available in all contexts\n }\n\n // Note: Search module configs are registered synchronously in the main bootstrap.\n // The actual registerSearchModule() call happens in core/bootstrap.ts when the\n // DI container is created, using getSearchModuleConfigs() from the global registry.\n\n // Note: CLI module registration is handled separately in CLI context\n // via bootstrapFromAppRoot in dynamicLoader. We don't import CLI here\n // to avoid Turbopack tracing through the CLI package in Next.js context.\n}\n\n/**\n * Check if bootstrap has been called.\n */\nexport function isBootstrapped(): boolean {\n return _bootstrapped\n}\n\n/**\n * Reset bootstrap state. Useful for testing.\n */\nexport function resetBootstrapState(): void {\n _bootstrapped = false\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { BootstrapData, BootstrapOptions } from './types'\nimport { registerOrmEntities } from '../db/mikro'\nimport { registerAppDiRegistrar, registerDiRegistrars } from '../di/container'\nimport { registerModules } from '../modules/registry'\nimport { registerEntityIds } from '../encryption/entityIds'\nimport { registerEntityFields } from '../encryption/entityFields'\nimport { registerSearchModuleConfigs } from '../../modules/search'\nimport { registerAnalyticsModuleConfigs } from '../../modules/analytics'\nimport { registerCodeWorkflowEntries } from '../../modules/workflows/code-registry'\nimport { registerResponseEnrichers } from '../crud/enricher-registry'\nimport { registerApiInterceptors } from '../crud/interceptor-registry'\nimport { registerComponentOverrides } from '../../modules/widgets/component-registry'\nimport { registerMutationGuards } from '../crud/mutation-guard-store'\nimport { registerCommandInterceptors } from '../commands/command-interceptor-store'\nimport { registerCommandLoaders } from '../commands/registry'\nimport { registerNotificationHandlers } from '../notifications/handler-registry'\nimport { clearRegisteredIntegrations, registerBundles, registerIntegrations } from '../../modules/integrations/types'\nimport { applyComponentOverridesToEntries } from '../../modules/overrides'\n\nlet _bootstrapped = false\n\n// Store the async registration promise so callers can await it if needed\nlet _asyncRegistrationPromise: Promise<void> | null = null\n\n/**\n * Creates a bootstrap function that registers all application dependencies.\n *\n * The returned function should be called once at application startup.\n * In development mode, it can be called multiple times (for HMR).\n *\n * @param data - All generated registry data from .mercato/generated/\n * @param options - Optional configuration\n * @returns A bootstrap function to call at app startup\n */\nexport function createBootstrap(data: BootstrapData, options: BootstrapOptions = {}) {\n return function bootstrap(): void {\n if (options.appDiRegistrar) {\n registerAppDiRegistrar(options.appDiRegistrar)\n }\n // In development, always re-run registrations to handle HMR\n // (Module state may be reset when Turbopack reloads packages)\n if (_bootstrapped && process.env.NODE_ENV !== 'development') return\n _bootstrapped = true\n\n // === 1. Foundation: ORM entities and DI registrars ===\n registerOrmEntities(data.entities)\n registerDiRegistrars(data.diRegistrars.filter((r): r is NonNullable<typeof r> => r != null))\n\n // === 2. Modules registry (required by i18n, query engine, dashboards, CLI) ===\n registerModules(data.modules)\n clearRegisteredIntegrations()\n for (const module of data.modules) {\n if (module.integrations?.length) {\n registerIntegrations(module.integrations)\n }\n if (module.bundles?.length) {\n registerBundles(module.bundles)\n }\n }\n\n // === 3. Entity IDs (required by encryption, indexing, entity links) ===\n registerEntityIds(data.entityIds)\n\n // === 4. Entity fields registry (for encryption manager, Turbopack compatibility) ===\n if (data.entityFieldsRegistry) {\n registerEntityFields(data.entityFieldsRegistry)\n }\n\n // === 5. Search module configs (for search service registration in DI) ===\n if (data.searchModuleConfigs) {\n registerSearchModuleConfigs(data.searchModuleConfigs)\n }\n\n // === 6. Analytics module configs (for dashboard widgets and analytics API) ===\n if (data.analyticsModuleConfigs) {\n registerAnalyticsModuleConfigs(data.analyticsModuleConfigs)\n }\n\n // === 6a. Code workflow definitions (so CLI/worker processes resolve them like the app runtime) ===\n if (data.codeWorkflows?.length) {\n registerCodeWorkflowEntries(data.codeWorkflows)\n }\n\n // === 6b. Response enrichers (for CRUD response enrichment) ===\n if (data.enricherEntries) {\n registerResponseEnrichers(data.enricherEntries)\n }\n\n // === 6c. API interceptors (for CRUD route interception) ===\n if (data.interceptorEntries) {\n registerApiInterceptors(data.interceptorEntries)\n }\n\n // === 6d. Component overrides (for page/component replacement) ===\n if (data.componentOverrideEntries) {\n const finalEntries = applyComponentOverridesToEntries(data.componentOverrideEntries)\n const allOverrides = finalEntries.flatMap((entry) => entry.componentOverrides ?? [])\n registerComponentOverrides(allOverrides)\n }\n\n // === 6e. Mutation guards (for CRUD mutation lifecycle) ===\n if (data.guardEntries) {\n registerMutationGuards(data.guardEntries)\n }\n\n // === 6f. Command interceptors (for command bus lifecycle) ===\n if (data.commandInterceptorEntries) {\n registerCommandInterceptors(data.commandInterceptorEntries)\n }\n\n // === 6f.1. Command loaders (for lazy command handler registration) ===\n if (data.commandLoaderEntries) {\n registerCommandLoaders(data.commandLoaderEntries)\n }\n\n // === 6g. Notification handlers (reactive notification side-effects) ===\n if (data.notificationHandlerEntries) {\n registerNotificationHandlers(data.notificationHandlerEntries)\n }\n\n // === 7-8. UI Widgets and Optional packages (async to avoid circular deps) ===\n // Store the promise so CLI context can await it\n _asyncRegistrationPromise = registerWidgetsAndOptionalPackages(data, options)\n void _asyncRegistrationPromise\n\n options.onRegistrationComplete?.()\n }\n}\n\n/**\n * Wait for async registrations (CLI modules, widgets, etc.) to complete.\n * Call this after bootstrap() in CLI context where you need modules immediately.\n */\nexport async function waitForAsyncRegistration(): Promise<void> {\n if (_asyncRegistrationPromise) {\n await _asyncRegistrationPromise\n }\n}\n\nasync function registerWidgetsAndOptionalPackages(data: BootstrapData, options: BootstrapOptions): Promise<void> {\n // Register UI widgets (dynamic imports to avoid circular deps with ui/core packages)\n try {\n const [dashboardRegistry, injectionRegistry, coreInjection] = await Promise.all([\n import('@open-mercato/ui/backend/dashboard/widgetRegistry'),\n import('@open-mercato/ui/backend/injection/widgetRegistry'),\n import('@open-mercato/core/modules/widgets/lib/injection'),\n ])\n\n dashboardRegistry.registerDashboardWidgets(data.dashboardWidgetEntries)\n injectionRegistry.registerInjectionWidgets(data.injectionWidgetEntries)\n coreInjection.registerCoreInjectionWidgets(data.injectionWidgetEntries)\n coreInjection.registerCoreInjectionTables(data.injectionTables)\n coreInjection.registerEnabledModuleIds(\n data.modules.map((module) => module.id).filter((id): id is string => typeof id === 'string' && id.length > 0),\n )\n } catch {\n // UI packages may not be available in all contexts\n }\n\n // Note: Search module configs are registered synchronously in the main bootstrap.\n // The actual registerSearchModule() call happens in core/bootstrap.ts when the\n // DI container is created, using getSearchModuleConfigs() from the global registry.\n\n // Note: CLI module registration is handled separately in CLI context\n // via bootstrapFromAppRoot in dynamicLoader. We don't import CLI here\n // to avoid Turbopack tracing through the CLI package in Next.js context.\n}\n\n/**\n * Check if bootstrap has been called.\n */\nexport function isBootstrapped(): boolean {\n return _bootstrapped\n}\n\n/**\n * Reset bootstrap state. Useful for testing.\n */\nexport function resetBootstrapState(): void {\n _bootstrapped = false\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { hasAllFeatures } from "../../security/features.js";
2
+ import { resolveCrudMutationGuardService } from "./mutation-guard-service.js";
2
3
  function matchesEntity(pattern, entity) {
3
4
  if (pattern === "*") return true;
4
5
  if (pattern === entity) return true;
@@ -25,19 +26,8 @@ async function runMutationGuards(guards, input, context) {
25
26
  }
26
27
  return { ok: true, modifiedPayload: payload !== input.mutationPayload ? payload ?? void 0 : void 0, afterSuccessCallbacks };
27
28
  }
28
- function resolveLegacyGuardService(container) {
29
- try {
30
- const service = container.resolve("crudMutationGuardService");
31
- if (!service) return null;
32
- if (typeof service.validateMutation !== "function") return null;
33
- if (typeof service.afterMutationSuccess !== "function") return null;
34
- return service;
35
- } catch {
36
- return null;
37
- }
38
- }
39
29
  function bridgeLegacyGuard(container) {
40
- const legacyService = resolveLegacyGuardService(container);
30
+ const legacyService = resolveCrudMutationGuardService(container);
41
31
  if (!legacyService) return null;
42
32
  return {
43
33
  id: "_legacy.crud-mutation-guard-service",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/mutation-guard-registry.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { hasAllFeatures } from '../../security/features'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MutationGuard {\n /** Unique guard ID (e.g., 'record_locks.lock-check', 'example.todo-limit') */\n id: string\n\n /** Target entity or '*' for all entities */\n targetEntity: string | '*'\n\n /** Which operations this guard applies to */\n operations: ('create' | 'update' | 'delete')[]\n\n /** Execution priority (lower = earlier). Default: 50 */\n priority?: number\n\n /** ACL feature gating \u2014 guard only runs if user has these features */\n features?: string[]\n\n /** Validate before mutation. Return ok:false to block, modifiedPayload to transform. */\n validate(input: MutationGuardInput): Promise<MutationGuardResult>\n\n /** Optional post-mutation callback (for cleanup, cache invalidation, etc.) */\n afterSuccess?(input: MutationGuardAfterInput): Promise<void>\n}\n\nexport interface MutationGuardInput {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string | null\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport interface MutationGuardResult {\n ok: boolean\n /** HTTP status for rejection (default: 422) */\n status?: number\n /** Error message for rejection */\n message?: string\n /** Full error body for rejection (overrides message) */\n body?: Record<string, unknown>\n /** Modified payload \u2014 merged into mutation data if ok:true */\n modifiedPayload?: Record<string, unknown>\n /** Should afterSuccess run? (default: false) */\n shouldRunAfterSuccess?: boolean\n /** Arbitrary metadata passed to afterSuccess */\n metadata?: Record<string, unknown>\n}\n\nexport interface MutationGuardAfterInput {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\n// ---------------------------------------------------------------------------\n// Entity matching\n// ---------------------------------------------------------------------------\n\nexport function matchesEntity(pattern: string, entity: string): boolean {\n if (pattern === '*') return true\n if (pattern === entity) return true\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2)\n return entity.startsWith(prefix + '.')\n }\n return false\n}\n\n// ---------------------------------------------------------------------------\n// Guard runner\n// ---------------------------------------------------------------------------\n\nexport async function runMutationGuards(\n guards: MutationGuard[],\n input: MutationGuardInput,\n context: { userFeatures: string[] },\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const matching = guards\n .filter((g) => matchesEntity(g.targetEntity, input.resourceKind))\n .filter((g) => g.operations.includes(input.operation))\n .filter((g) => hasAllFeatures(context.userFeatures, g.features))\n .sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50))\n\n let payload = input.mutationPayload\n const afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }> = []\n\n for (const guard of matching) {\n const result = await guard.validate({ ...input, mutationPayload: payload })\n if (!result.ok) {\n const body = result.body ?? { error: result.message ?? 'Operation blocked by guard', guardId: guard.id }\n return { ok: false, errorBody: body, errorStatus: result.status ?? 422, afterSuccessCallbacks: [] }\n }\n if (result.modifiedPayload) payload = { ...payload, ...result.modifiedPayload }\n if (result.shouldRunAfterSuccess && guard.afterSuccess) {\n afterSuccessCallbacks.push({ guard, metadata: result.metadata ?? null })\n }\n }\n\n return { ok: true, modifiedPayload: payload !== input.mutationPayload ? (payload ?? undefined) : undefined, afterSuccessCallbacks }\n}\n\n// ---------------------------------------------------------------------------\n// Legacy guard bridge\n// ---------------------------------------------------------------------------\n\ntype LegacyCrudMutationGuardService = {\n validateMutation: (input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n }) => Promise<{ ok: boolean; status?: number; body?: Record<string, unknown>; shouldRunAfterSuccess?: boolean; metadata?: Record<string, unknown> | null } | null>\n afterMutationSuccess: (input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n }) => Promise<void>\n}\n\nfunction resolveLegacyGuardService(container: AwilixContainer): LegacyCrudMutationGuardService | null {\n try {\n const service = container.resolve<LegacyCrudMutationGuardService>('crudMutationGuardService')\n if (!service) return null\n if (typeof service.validateMutation !== 'function') return null\n if (typeof service.afterMutationSuccess !== 'function') return null\n return service\n } catch {\n return null\n }\n}\n\nexport function bridgeLegacyGuard(container: AwilixContainer): MutationGuard | null {\n const legacyService = resolveLegacyGuardService(container)\n if (!legacyService) return null\n\n return {\n id: '_legacy.crud-mutation-guard-service',\n targetEntity: '*',\n operations: ['update', 'delete'],\n priority: 0,\n\n async validate(input) {\n const result = await legacyService.validateMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId ?? '',\n operation: input.operation,\n requestMethod: input.requestMethod,\n requestHeaders: input.requestHeaders,\n mutationPayload: input.mutationPayload,\n })\n if (!result) return { ok: true }\n if (!result.ok) return { ok: false, status: result.status, body: result.body }\n return { ok: true, shouldRunAfterSuccess: result.shouldRunAfterSuccess, metadata: result.metadata ?? undefined }\n },\n\n async afterSuccess(input) {\n await legacyService.afterMutationSuccess({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n requestMethod: input.requestMethod,\n requestHeaders: input.requestHeaders,\n metadata: input.metadata,\n })\n },\n }\n}\n"],
5
- "mappings": "AACA,SAAS,sBAAsB;AAyExB,SAAS,cAAc,SAAiB,QAAyB;AACtE,MAAI,YAAY,IAAK,QAAO;AAC5B,MAAI,YAAY,OAAQ,QAAO;AAC/B,MAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,UAAM,SAAS,QAAQ,MAAM,GAAG,EAAE;AAClC,WAAO,OAAO,WAAW,SAAS,GAAG;AAAA,EACvC;AACA,SAAO;AACT;AAMA,eAAsB,kBACpB,QACA,OACA,SAOC;AACD,QAAM,WAAW,OACd,OAAO,CAAC,MAAM,cAAc,EAAE,cAAc,MAAM,YAAY,CAAC,EAC/D,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS,MAAM,SAAS,CAAC,EACpD,OAAO,CAAC,MAAM,eAAe,QAAQ,cAAc,EAAE,QAAQ,CAAC,EAC9D,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,EAAE,YAAY,GAAG;AAEzD,MAAI,UAAU,MAAM;AACpB,QAAM,wBAAmG,CAAC;AAE1G,aAAW,SAAS,UAAU;AAC5B,UAAM,SAAS,MAAM,MAAM,SAAS,EAAE,GAAG,OAAO,iBAAiB,QAAQ,CAAC;AAC1E,QAAI,CAAC,OAAO,IAAI;AACd,YAAM,OAAO,OAAO,QAAQ,EAAE,OAAO,OAAO,WAAW,8BAA8B,SAAS,MAAM,GAAG;AACvG,aAAO,EAAE,IAAI,OAAO,WAAW,MAAM,aAAa,OAAO,UAAU,KAAK,uBAAuB,CAAC,EAAE;AAAA,IACpG;AACA,QAAI,OAAO,gBAAiB,WAAU,EAAE,GAAG,SAAS,GAAG,OAAO,gBAAgB;AAC9E,QAAI,OAAO,yBAAyB,MAAM,cAAc;AACtD,4BAAsB,KAAK,EAAE,OAAO,UAAU,OAAO,YAAY,KAAK,CAAC;AAAA,IACzE;AAAA,EACF;AAEA,SAAO,EAAE,IAAI,MAAM,iBAAiB,YAAY,MAAM,kBAAmB,WAAW,SAAa,QAAW,sBAAsB;AACpI;AA+BA,SAAS,0BAA0B,WAAmE;AACpG,MAAI;AACF,UAAM,UAAU,UAAU,QAAwC,0BAA0B;AAC5F,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,OAAO,QAAQ,qBAAqB,WAAY,QAAO;AAC3D,QAAI,OAAO,QAAQ,yBAAyB,WAAY,QAAO;AAC/D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,kBAAkB,WAAkD;AAClF,QAAM,gBAAgB,0BAA0B,SAAS;AACzD,MAAI,CAAC,cAAe,QAAO;AAE3B,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,cAAc;AAAA,IACd,YAAY,CAAC,UAAU,QAAQ;AAAA,IAC/B,UAAU;AAAA,IAEV,MAAM,SAAS,OAAO;AACpB,YAAM,SAAS,MAAM,cAAc,iBAAiB;AAAA,QAClD,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM,cAAc;AAAA,QAChC,WAAW,MAAM;AAAA,QACjB,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QACtB,iBAAiB,MAAM;AAAA,MACzB,CAAC;AACD,UAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,KAAK;AAC/B,UAAI,CAAC,OAAO,GAAI,QAAO,EAAE,IAAI,OAAO,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AAC7E,aAAO,EAAE,IAAI,MAAM,uBAAuB,OAAO,uBAAuB,UAAU,OAAO,YAAY,OAAU;AAAA,IACjH;AAAA,IAEA,MAAM,aAAa,OAAO;AACxB,YAAM,cAAc,qBAAqB;AAAA,QACvC,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { hasAllFeatures } from '../../security/features'\nimport { resolveCrudMutationGuardService } from './mutation-guard-service'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MutationGuard {\n /** Unique guard ID (e.g., 'record_locks.lock-check', 'example.todo-limit') */\n id: string\n\n /** Target entity or '*' for all entities */\n targetEntity: string | '*'\n\n /** Which operations this guard applies to */\n operations: ('create' | 'update' | 'delete')[]\n\n /** Execution priority (lower = earlier). Default: 50 */\n priority?: number\n\n /** ACL feature gating \u2014 guard only runs if user has these features */\n features?: string[]\n\n /** Validate before mutation. Return ok:false to block, modifiedPayload to transform. */\n validate(input: MutationGuardInput): Promise<MutationGuardResult>\n\n /** Optional post-mutation callback (for cleanup, cache invalidation, etc.) */\n afterSuccess?(input: MutationGuardAfterInput): Promise<void>\n}\n\nexport interface MutationGuardInput {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string | null\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport interface MutationGuardResult {\n ok: boolean\n /** HTTP status for rejection (default: 422) */\n status?: number\n /** Error message for rejection */\n message?: string\n /** Full error body for rejection (overrides message) */\n body?: Record<string, unknown>\n /** Modified payload \u2014 merged into mutation data if ok:true */\n modifiedPayload?: Record<string, unknown>\n /** Should afterSuccess run? (default: false) */\n shouldRunAfterSuccess?: boolean\n /** Arbitrary metadata passed to afterSuccess */\n metadata?: Record<string, unknown>\n}\n\nexport interface MutationGuardAfterInput {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\n// ---------------------------------------------------------------------------\n// Entity matching\n// ---------------------------------------------------------------------------\n\nexport function matchesEntity(pattern: string, entity: string): boolean {\n if (pattern === '*') return true\n if (pattern === entity) return true\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2)\n return entity.startsWith(prefix + '.')\n }\n return false\n}\n\n// ---------------------------------------------------------------------------\n// Guard runner\n// ---------------------------------------------------------------------------\n\nexport async function runMutationGuards(\n guards: MutationGuard[],\n input: MutationGuardInput,\n context: { userFeatures: string[] },\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const matching = guards\n .filter((g) => matchesEntity(g.targetEntity, input.resourceKind))\n .filter((g) => g.operations.includes(input.operation))\n .filter((g) => hasAllFeatures(context.userFeatures, g.features))\n .sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50))\n\n let payload = input.mutationPayload\n const afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }> = []\n\n for (const guard of matching) {\n const result = await guard.validate({ ...input, mutationPayload: payload })\n if (!result.ok) {\n const body = result.body ?? { error: result.message ?? 'Operation blocked by guard', guardId: guard.id }\n return { ok: false, errorBody: body, errorStatus: result.status ?? 422, afterSuccessCallbacks: [] }\n }\n if (result.modifiedPayload) payload = { ...payload, ...result.modifiedPayload }\n if (result.shouldRunAfterSuccess && guard.afterSuccess) {\n afterSuccessCallbacks.push({ guard, metadata: result.metadata ?? null })\n }\n }\n\n return { ok: true, modifiedPayload: payload !== input.mutationPayload ? (payload ?? undefined) : undefined, afterSuccessCallbacks }\n}\n\n// ---------------------------------------------------------------------------\n// Legacy guard bridge\n// ---------------------------------------------------------------------------\n\nexport function bridgeLegacyGuard(container: AwilixContainer): MutationGuard | null {\n const legacyService = resolveCrudMutationGuardService(container)\n if (!legacyService) return null\n\n return {\n id: '_legacy.crud-mutation-guard-service',\n targetEntity: '*',\n operations: ['update', 'delete'],\n priority: 0,\n\n async validate(input) {\n const result = await legacyService.validateMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId ?? '',\n operation: input.operation,\n requestMethod: input.requestMethod,\n requestHeaders: input.requestHeaders,\n mutationPayload: input.mutationPayload,\n })\n if (!result) return { ok: true }\n if (!result.ok) return { ok: false, status: result.status, body: result.body }\n return { ok: true, shouldRunAfterSuccess: result.shouldRunAfterSuccess, metadata: result.metadata ?? undefined }\n },\n\n async afterSuccess(input) {\n await legacyService.afterMutationSuccess({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n requestMethod: input.requestMethod,\n requestHeaders: input.requestHeaders,\n metadata: input.metadata,\n })\n },\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,sBAAsB;AAC/B,SAAS,uCAAuC;AAyEzC,SAAS,cAAc,SAAiB,QAAyB;AACtE,MAAI,YAAY,IAAK,QAAO;AAC5B,MAAI,YAAY,OAAQ,QAAO;AAC/B,MAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,UAAM,SAAS,QAAQ,MAAM,GAAG,EAAE;AAClC,WAAO,OAAO,WAAW,SAAS,GAAG;AAAA,EACvC;AACA,SAAO;AACT;AAMA,eAAsB,kBACpB,QACA,OACA,SAOC;AACD,QAAM,WAAW,OACd,OAAO,CAAC,MAAM,cAAc,EAAE,cAAc,MAAM,YAAY,CAAC,EAC/D,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS,MAAM,SAAS,CAAC,EACpD,OAAO,CAAC,MAAM,eAAe,QAAQ,cAAc,EAAE,QAAQ,CAAC,EAC9D,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,EAAE,YAAY,GAAG;AAEzD,MAAI,UAAU,MAAM;AACpB,QAAM,wBAAmG,CAAC;AAE1G,aAAW,SAAS,UAAU;AAC5B,UAAM,SAAS,MAAM,MAAM,SAAS,EAAE,GAAG,OAAO,iBAAiB,QAAQ,CAAC;AAC1E,QAAI,CAAC,OAAO,IAAI;AACd,YAAM,OAAO,OAAO,QAAQ,EAAE,OAAO,OAAO,WAAW,8BAA8B,SAAS,MAAM,GAAG;AACvG,aAAO,EAAE,IAAI,OAAO,WAAW,MAAM,aAAa,OAAO,UAAU,KAAK,uBAAuB,CAAC,EAAE;AAAA,IACpG;AACA,QAAI,OAAO,gBAAiB,WAAU,EAAE,GAAG,SAAS,GAAG,OAAO,gBAAgB;AAC9E,QAAI,OAAO,yBAAyB,MAAM,cAAc;AACtD,4BAAsB,KAAK,EAAE,OAAO,UAAU,OAAO,YAAY,KAAK,CAAC;AAAA,IACzE;AAAA,EACF;AAEA,SAAO,EAAE,IAAI,MAAM,iBAAiB,YAAY,MAAM,kBAAmB,WAAW,SAAa,QAAW,sBAAsB;AACpI;AAMO,SAAS,kBAAkB,WAAkD;AAClF,QAAM,gBAAgB,gCAAgC,SAAS;AAC/D,MAAI,CAAC,cAAe,QAAO;AAE3B,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,cAAc;AAAA,IACd,YAAY,CAAC,UAAU,QAAQ;AAAA,IAC/B,UAAU;AAAA,IAEV,MAAM,SAAS,OAAO;AACpB,YAAM,SAAS,MAAM,cAAc,iBAAiB;AAAA,QAClD,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM,cAAc;AAAA,QAChC,WAAW,MAAM;AAAA,QACjB,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QACtB,iBAAiB,MAAM;AAAA,MACzB,CAAC;AACD,UAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,KAAK;AAC/B,UAAI,CAAC,OAAO,GAAI,QAAO,EAAE,IAAI,OAAO,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AAC7E,aAAO,EAAE,IAAI,MAAM,uBAAuB,OAAO,uBAAuB,UAAU,OAAO,YAAY,OAAU;AAAA,IACjH;AAAA,IAEA,MAAM,aAAa,OAAO;AACxB,YAAM,cAAc,qBAAqB;AAAA,QACvC,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,30 @@
1
+ import { createLogger } from "../logger/index.js";
2
+ const logger = createLogger("shared").child({ component: "crud" });
3
+ const RESOLUTION_WARNING_KEY = "__openMercatoCrudMutationGuardResolutionWarningEmitted__";
4
+ function warnResolutionFailureOnce(error) {
5
+ const globalScope = globalThis;
6
+ if (globalScope[RESOLUTION_WARNING_KEY] === true) return;
7
+ globalScope[RESOLUTION_WARNING_KEY] = true;
8
+ logger.warn("CRUD mutation guard service could not be resolved; the legacy guard bridge is disabled", {
9
+ err: error
10
+ });
11
+ }
12
+ function resolveCrudMutationGuardService(container) {
13
+ if (typeof container.hasRegistration === "function" && !container.hasRegistration("crudMutationGuardService")) {
14
+ return null;
15
+ }
16
+ try {
17
+ const service = container.resolve("crudMutationGuardService");
18
+ if (!service) return null;
19
+ if (typeof service.validateMutation !== "function") return null;
20
+ if (typeof service.afterMutationSuccess !== "function") return null;
21
+ return service;
22
+ } catch (error) {
23
+ warnResolutionFailureOnce(error);
24
+ return null;
25
+ }
26
+ }
27
+ export {
28
+ resolveCrudMutationGuardService
29
+ };
30
+ //# sourceMappingURL=mutation-guard-service.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/crud/mutation-guard-service.ts"],
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { createLogger } from '../logger'\nimport type {\n CrudMutationGuardAfterSuccessInput,\n CrudMutationGuardValidateInput,\n CrudMutationGuardValidationResult,\n} from './mutation-guard'\n\nconst logger = createLogger('shared').child({ component: 'crud' })\nconst RESOLUTION_WARNING_KEY = '__openMercatoCrudMutationGuardResolutionWarningEmitted__'\n\nexport type CrudMutationGuardServiceLike = {\n validateMutation: (\n input: CrudMutationGuardValidateInput,\n ) => Promise<CrudMutationGuardValidationResult | null>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction warnResolutionFailureOnce(error: unknown): void {\n const globalScope = globalThis as Record<string, unknown>\n if (globalScope[RESOLUTION_WARNING_KEY] === true) return\n globalScope[RESOLUTION_WARNING_KEY] = true\n logger.warn('CRUD mutation guard service could not be resolved; the legacy guard bridge is disabled', {\n err: error,\n })\n}\n\nexport function resolveCrudMutationGuardService(\n container: AwilixContainer,\n): CrudMutationGuardServiceLike | null {\n if (\n typeof container.hasRegistration === 'function'\n && !container.hasRegistration('crudMutationGuardService')\n ) {\n return null\n }\n\n try {\n const service = container.resolve<CrudMutationGuardServiceLike>('crudMutationGuardService')\n if (!service) return null\n if (typeof service.validateMutation !== 'function') return null\n if (typeof service.afterMutationSuccess !== 'function') return null\n return service\n } catch (error) {\n warnResolutionFailureOnce(error)\n return null\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,oBAAoB;AAO7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,OAAO,CAAC;AACjE,MAAM,yBAAyB;AAS/B,SAAS,0BAA0B,OAAsB;AACvD,QAAM,cAAc;AACpB,MAAI,YAAY,sBAAsB,MAAM,KAAM;AAClD,cAAY,sBAAsB,IAAI;AACtC,SAAO,KAAK,0FAA0F;AAAA,IACpG,KAAK;AAAA,EACP,CAAC;AACH;AAEO,SAAS,gCACd,WACqC;AACrC,MACE,OAAO,UAAU,oBAAoB,cAClC,CAAC,UAAU,gBAAgB,0BAA0B,GACxD;AACA,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,UAAU,UAAU,QAAsC,0BAA0B;AAC1F,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,OAAO,QAAQ,qBAAqB,WAAY,QAAO;AAC3D,QAAI,OAAO,QAAQ,yBAAyB,WAAY,QAAO;AAC/D,WAAO;AAAA,EACT,SAAS,OAAO;AACd,8BAA0B,KAAK;AAC/B,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -1,16 +1,6 @@
1
1
  import { createLogger } from "../logger/index.js";
2
+ import { resolveCrudMutationGuardService } from "./mutation-guard-service.js";
2
3
  const logger = createLogger("shared").child({ component: "crud" });
3
- function resolveCrudMutationGuardService(container) {
4
- try {
5
- const service = container.resolve("crudMutationGuardService");
6
- if (!service) return null;
7
- if (typeof service.validateMutation !== "function") return null;
8
- if (typeof service.afterMutationSuccess !== "function") return null;
9
- return service;
10
- } catch {
11
- return null;
12
- }
13
- }
14
4
  async function validateCrudMutationGuard(container, input) {
15
5
  const service = resolveCrudMutationGuardService(container);
16
6
  if (!service) return null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/mutation-guard.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'crud' })\n\nexport type CrudMutationGuardValidationSuccess = {\n ok: true\n shouldRunAfterSuccess: boolean\n metadata?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardValidationFailure = {\n ok: false\n status: number\n body: Record<string, unknown>\n}\n\nexport type CrudMutationGuardValidationResult =\n | CrudMutationGuardValidationSuccess\n | CrudMutationGuardValidationFailure\n\nexport type CrudMutationGuardValidateInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardAfterSuccessInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\ntype CrudMutationGuardServiceLike = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction resolveCrudMutationGuardService(container: AwilixContainer): CrudMutationGuardServiceLike | null {\n try {\n const service = container.resolve<CrudMutationGuardServiceLike>('crudMutationGuardService')\n if (!service) return null\n if (typeof service.validateMutation !== 'function') return null\n if (typeof service.afterMutationSuccess !== 'function') return null\n return service\n } catch {\n return null\n }\n}\n\n/**\n * @deprecated Resolves ONLY the single DI-registered `crudMutationGuardService`,\n * so it silently bypasses every guard in the global mutation-guard store\n * (`getAllMutationGuardInstances()`). Use the full registry instead:\n * `runRouteMutationGuards()` from `@open-mercato/shared/lib/crud/route-mutation-guard`\n * for custom write routes, or `runMutationGuards()` from\n * `@open-mercato/shared/lib/crud/mutation-guard-registry` directly. The legacy\n * service is still honored on the modern path via `bridgeLegacyGuard()`. This\n * function will be removed in a future release.\n */\nexport async function validateCrudMutationGuard(\n container: AwilixContainer,\n input: CrudMutationGuardValidateInput,\n): Promise<CrudMutationGuardValidationResult | null> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return null\n return service.validateMutation(input)\n}\n\n/**\n * @deprecated Runs ONLY the single DI-registered `crudMutationGuardService`'s\n * after-success hook, skipping the registry guards' `afterSuccess` callbacks.\n * Use the `runAfterSuccess()` returned by `runRouteMutationGuards()` from\n * `@open-mercato/shared/lib/crud/route-mutation-guard`, or the\n * `afterSuccessCallbacks` returned by `runMutationGuards()` from\n * `@open-mercato/shared/lib/crud/mutation-guard-registry`. This function will be\n * removed in a future release.\n */\nexport async function runCrudMutationGuardAfterSuccess(\n container: AwilixContainer,\n input: CrudMutationGuardAfterSuccessInput,\n): Promise<void> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return\n try {\n await service.afterMutationSuccess(input)\n } catch (error) {\n logger.error('Mutation guard after-success hook failed', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n requestMethod: input.requestMethod,\n err: error,\n })\n }\n}\n"],
5
- "mappings": "AACA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,OAAO,CAAC;AA+CjE,SAAS,gCAAgC,WAAiE;AACxG,MAAI;AACF,UAAM,UAAU,UAAU,QAAsC,0BAA0B;AAC1F,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,OAAO,QAAQ,qBAAqB,WAAY,QAAO;AAC3D,QAAI,OAAO,QAAQ,yBAAyB,WAAY,QAAO;AAC/D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYA,eAAsB,0BACpB,WACA,OACmD;AACnD,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,iBAAiB,KAAK;AACvC;AAWA,eAAsB,iCACpB,WACA,OACe;AACf,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS;AACd,MAAI;AACF,UAAM,QAAQ,qBAAqB,KAAK;AAAA,EAC1C,SAAS,OAAO;AACd,WAAO,MAAM,4CAA4C;AAAA,MACvD,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,eAAe,MAAM;AAAA,MACrB,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { createLogger } from '../logger'\nimport { resolveCrudMutationGuardService } from './mutation-guard-service'\n\nconst logger = createLogger('shared').child({ component: 'crud' })\n\nexport type CrudMutationGuardValidationSuccess = {\n ok: true\n shouldRunAfterSuccess: boolean\n metadata?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardValidationFailure = {\n ok: false\n status: number\n body: Record<string, unknown>\n}\n\nexport type CrudMutationGuardValidationResult =\n | CrudMutationGuardValidationSuccess\n | CrudMutationGuardValidationFailure\n\nexport type CrudMutationGuardValidateInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type CrudMutationGuardAfterSuccessInput = {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete' | 'custom'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\n/**\n * @deprecated Resolves ONLY the single DI-registered `crudMutationGuardService`,\n * so it silently bypasses every guard in the global mutation-guard store\n * (`getAllMutationGuardInstances()`). Use the full registry instead:\n * `runRouteMutationGuards()` from `@open-mercato/shared/lib/crud/route-mutation-guard`\n * for custom write routes, or `runMutationGuards()` from\n * `@open-mercato/shared/lib/crud/mutation-guard-registry` directly. The legacy\n * service is still honored on the modern path via `bridgeLegacyGuard()`. This\n * function will be removed in a future release.\n */\nexport async function validateCrudMutationGuard(\n container: AwilixContainer,\n input: CrudMutationGuardValidateInput,\n): Promise<CrudMutationGuardValidationResult | null> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return null\n return service.validateMutation(input)\n}\n\n/**\n * @deprecated Runs ONLY the single DI-registered `crudMutationGuardService`'s\n * after-success hook, skipping the registry guards' `afterSuccess` callbacks.\n * Use the `runAfterSuccess()` returned by `runRouteMutationGuards()` from\n * `@open-mercato/shared/lib/crud/route-mutation-guard`, or the\n * `afterSuccessCallbacks` returned by `runMutationGuards()` from\n * `@open-mercato/shared/lib/crud/mutation-guard-registry`. This function will be\n * removed in a future release.\n */\nexport async function runCrudMutationGuardAfterSuccess(\n container: AwilixContainer,\n input: CrudMutationGuardAfterSuccessInput,\n): Promise<void> {\n const service = resolveCrudMutationGuardService(container)\n if (!service) return\n try {\n await service.afterMutationSuccess(input)\n } catch (error) {\n logger.error('Mutation guard after-success hook failed', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n requestMethod: input.requestMethod,\n err: error,\n })\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,uCAAuC;AAEhD,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,OAAO,CAAC;AAoDjE,eAAsB,0BACpB,WACA,OACmD;AACnD,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,iBAAiB,KAAK;AACvC;AAWA,eAAsB,iCACpB,WACA,OACe;AACf,QAAM,UAAU,gCAAgC,SAAS;AACzD,MAAI,CAAC,QAAS;AACd,MAAI;AACF,UAAM,QAAQ,qBAAqB,KAAK;AAAA,EAC1C,SAAS,OAAO;AACd,WAAO,MAAM,4CAA4C;AAAA,MACvD,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,eAAe,MAAM;AAAA,MACrB,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACF;",
6
6
  "names": []
7
7
  }
@@ -11,6 +11,7 @@ import { createCommandOptimisticLockGuardService } from "@open-mercato/shared/li
11
11
  import { createLogger } from "../logger/index.js";
12
12
  const logger = createLogger("shared").child({ component: "di" });
13
13
  const GLOBAL_KEY = "__openMercatoDiRegistrars__";
14
+ const APP_DI_REGISTRAR_KEY = "__openMercatoAppDiRegistrar__";
14
15
  const BOOTSTRAP_CACHE_KEY = "__openMercatoBootstrapCache__";
15
16
  const ENCRYPTION_ENABLED_KEY = "__openMercatoEncryptionEnabledCache__";
16
17
  const BOOTSTRAP_CACHE_KEYS = [
@@ -84,6 +85,14 @@ function getDiRegistrars() {
84
85
  }
85
86
  return registrars;
86
87
  }
88
+ function getAppDiRegistrar() {
89
+ const registrar = globalThis[APP_DI_REGISTRAR_KEY];
90
+ return typeof registrar === "function" ? registrar : null;
91
+ }
92
+ function registerAppDiRegistrar(registrar) {
93
+ ;
94
+ globalThis[APP_DI_REGISTRAR_KEY] = registrar;
95
+ }
87
96
  function resetBootstrapCache() {
88
97
  globalThis[BOOTSTRAP_CACHE_KEY] = null;
89
98
  globalThis[ENCRYPTION_ENABLED_KEY] = void 0;
@@ -126,8 +135,8 @@ async function createRequestContainer() {
126
135
  // see the enterprise `record_locks` module for the canonical override.
127
136
  // Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md
128
137
  crudMutationGuardService: asFunction(
129
- ({ em: scopedEm }) => createOptimisticLockGuardService({
130
- getEm: () => scopedEm,
138
+ (em2) => createOptimisticLockGuardService({
139
+ getEm: () => em2,
131
140
  readers: getAllOptimisticLockReaders()
132
141
  })
133
142
  ).scoped(),
@@ -168,16 +177,26 @@ async function createRequestContainer() {
168
177
  }
169
178
  }
170
179
  }
171
- try {
172
- const appDi = await import("@/di");
173
- if (appDi?.register) {
174
- try {
175
- const maybe = appDi.register(container);
176
- if (maybe && typeof maybe.then === "function") await maybe;
177
- } catch {
180
+ const appDiRegistrar = getAppDiRegistrar();
181
+ if (appDiRegistrar) {
182
+ try {
183
+ await appDiRegistrar(container);
184
+ } catch (error) {
185
+ logger.error("App-level DI registrar failed", { err: error });
186
+ }
187
+ } else {
188
+ try {
189
+ const appDi = await import("@/di");
190
+ if (appDi?.register) {
191
+ try {
192
+ const maybe = appDi.register(container);
193
+ if (maybe && typeof maybe.then === "function") await maybe;
194
+ } catch (error) {
195
+ logger.error("App-level DI registrar failed", { err: error });
196
+ }
178
197
  }
198
+ } catch {
179
199
  }
180
- } catch {
181
200
  }
182
201
  applyDiOverridesToContainer({
183
202
  register: (registrations) => container.register(toAwilixRegistrations(registrations)),
@@ -201,6 +220,7 @@ try {
201
220
  export {
202
221
  createRequestContainer,
203
222
  getDiRegistrars,
223
+ registerAppDiRegistrar,
204
224
  registerDiRegistrars,
205
225
  resetBootstrapCache
206
226
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/di/container.ts"],
4
- "sourcesContent": ["import { asFunction, createContainer, asValue, AwilixContainer, InjectionMode, type Resolver } from 'awilix'\nimport { RequestContext } from '@mikro-orm/core'\nimport { getOrm } from '@open-mercato/shared/lib/db/mikro'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { BasicQueryEngine } from '@open-mercato/shared/lib/query/engine'\nimport { DefaultDataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { commandRegistry, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { applyDiOverridesToContainer } from '@open-mercato/shared/modules/overrides'\nimport { createOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport { createCommandOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'di' })\n\ntype DynamicCradle = Record<string, any>\n\nexport type AppContainer = AwilixContainer<DynamicCradle>\nexport type DiRegistrar = (container: AppContainer) => void\n\n// Registration pattern for publishable packages\n// Use globalThis to survive tsx/esbuild module duplication issue where the same\n// file can be loaded as multiple module instances when mixing dynamic and static imports\nconst GLOBAL_KEY = '__openMercatoDiRegistrars__'\n// Phase 5 \u2014 process-scoped bootstrap cache. The cache/event-bus/encryption\n// services bootstrap() creates are inherently process-scoped (they hold\n// state across requests). Caching them on globalThis after the first\n// successful bootstrap call lets every subsequent request skip the\n// `await bootstrap(container)` body and just re-register the cached\n// instances. Same globalThis pattern as registerDiRegistrars so HMR\n// keeps working.\nconst BOOTSTRAP_CACHE_KEY = '__openMercatoBootstrapCache__'\nconst ENCRYPTION_ENABLED_KEY = '__openMercatoEncryptionEnabledCache__'\n\nconst BOOTSTRAP_CACHE_KEYS = [\n 'cache',\n 'eventBus',\n 'kmsService',\n 'tenantEncryptionService',\n 'rateLimiterService',\n 'searchModuleConfigs',\n 'searchIndexer',\n] as const\n\ntype BootstrapCacheEntry = Partial<Record<(typeof BOOTSTRAP_CACHE_KEYS)[number], unknown>>\n\n// Phase 5 is opt-in. Some bootstrap services close over per-request state\n// (e.g. tenantEncryptionService captures the first request's `em.fork`, the\n// event-bus's resolver closes over the first container) so naively replaying\n// them on later requests yields stale references \u2014 observed as a 500 from\n// CRUD list endpoints in `next start`. Default OFF preserves develop's\n// per-request bootstrap. Set `OM_BOOTSTRAP_CACHE=1` to opt in once each\n// cached service is verified safe for cross-request reuse.\nfunction isBootstrapCacheEnabled(): boolean {\n const raw = process.env.OM_BOOTSTRAP_CACHE\n if (raw === undefined) return false\n const normalized = raw.trim().toLowerCase()\n if (!normalized.length) return false\n if (normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'no') return false\n return true\n}\n\nfunction getBootstrapCache(): BootstrapCacheEntry | null {\n if (!isBootstrapCacheEnabled()) return null\n const existing = (globalThis as any)[BOOTSTRAP_CACHE_KEY]\n return existing && typeof existing === 'object' ? (existing as BootstrapCacheEntry) : null\n}\n\nfunction setBootstrapCache(entry: BootstrapCacheEntry): void {\n if (!isBootstrapCacheEnabled()) return\n ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = entry\n}\n\nfunction harvestBootstrapCache(container: AwilixContainer): BootstrapCacheEntry {\n const entry: BootstrapCacheEntry = {}\n for (const key of BOOTSTRAP_CACHE_KEYS) {\n try {\n const value: unknown = container.resolve(key as never)\n if (value !== undefined && value !== null) entry[key] = value\n } catch {\n // not registered \u2014 skip\n }\n }\n return entry\n}\n\ntype EncryptionEnabledProbe = { isEnabled?: () => boolean } | null | undefined\n\nfunction getCachedEncryptionEnabled(service: EncryptionEnabledProbe): boolean | null {\n if (!service || typeof service.isEnabled !== 'function') return false\n const cached = (globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY]\n if (typeof cached === 'boolean') return cached\n try {\n const result = !!service.isEnabled()\n ;(globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY] = result\n return result\n } catch {\n return null\n }\n}\n\nfunction getGlobalRegistrars(): DiRegistrar[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalRegistrars(registrars: DiRegistrar[]): void {\n (globalThis as any)[GLOBAL_KEY] = registrars\n}\n\nexport function registerDiRegistrars(registrars: DiRegistrar[]) {\n const existing = getGlobalRegistrars()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n logger.debug('DI registrars re-registered (this may occur during HMR)')\n }\n setGlobalRegistrars(registrars)\n // Force re-bootstrap on HMR \u2014 module subscribers may have changed.\n ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = null\n ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined\n}\n\nexport function getDiRegistrars(): DiRegistrar[] {\n const registrars = getGlobalRegistrars()\n if (!registrars) {\n throw new Error('[Bootstrap] DI registrars not registered. Call registerDiRegistrars() at bootstrap.')\n }\n return registrars\n}\n\n/** Test-only helper to drop the process-scoped bootstrap cache. */\nexport function resetBootstrapCache(): void {\n (globalThis as any)[BOOTSTRAP_CACHE_KEY] = null\n ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined\n}\n\nfunction isAwilixResolver(value: unknown): value is Resolver<unknown> {\n return Boolean(value && typeof value === 'object' && typeof (value as { resolve?: unknown }).resolve === 'function')\n}\n\nfunction toAwilixRegistrations(registrations: Record<string, unknown>): Record<string, Resolver<any>> {\n return Object.fromEntries(\n Object.entries(registrations).map(([key, value]) => [\n key,\n isAwilixResolver(value) ? value : asValue(value),\n ]),\n )\n}\n\nexport async function createRequestContainer(): Promise<AppContainer> {\n const diRegistrars = getDiRegistrars()\n const orm = await getOrm()\n // Use a fresh event manager so request-level subscribers (e.g., encryption) don't pile up globally\n const baseEm = (RequestContext.getEntityManager() as any) ?? orm.em\n const em = baseEm.fork({ clear: true, freshEventManager: true, useContext: true }) as unknown as EntityManager\n const container = createContainer<DynamicCradle>({ injectionMode: InjectionMode.CLASSIC })\n // Core registrations\n container.register({\n em: asValue(em),\n queryEngine: asValue(new BasicQueryEngine(em, undefined, () => {\n try { return container.resolve('tenantEncryptionService') as any } catch { return null }\n })),\n dataEngine: asValue(new DefaultDataEngine(em, container as any)),\n commandRegistry: asValue(commandRegistry),\n commandBus: asValue(new CommandBus()),\n // Default OSS optimistic-lock guard. Reads from the global reader store\n // (populated by `makeCrudRoute` auto-registration + any module-DI\n // hand-wired calls to `registerOptimisticLockReaders`). Service is\n // strictly additive: when `OM_OPTIMISTIC_LOCK=off` (or no header is\n // sent) it short-circuits at validateMutation. Module-level di.ts\n // registrations override this default via Awilix replace semantics \u2014\n // see the enterprise `record_locks` module for the canonical override.\n // Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md\n crudMutationGuardService: asFunction(({ em: scopedEm }: { em: EntityManager }) =>\n createOptimisticLockGuardService({\n getEm: () => scopedEm,\n readers: getAllOptimisticLockReaders(),\n }),\n ).scoped(),\n // Default OSS command-level optimistic-lock guard, awaited by\n // `enforceCommandOptimisticLockWithGuards` for Command-pattern writes.\n // Header/explicit-token compare only (no `resolveExpected`), so it is\n // behaviourally identical to calling `enforceCommandOptimisticLock`\n // directly. The enterprise `record_locks` module overrides this DI key\n // with a lock-backed `resolveExpected` via Awilix replace semantics.\n // Spec: .ai/specs/enterprise/2026-06-09-record-locks-unified-coverage.md (Phase 0)\n commandOptimisticLockGuardService: asFunction(() =>\n createCommandOptimisticLockGuardService(),\n ).scoped(),\n })\n // Allow modules to override/extend\n for (const reg of diRegistrars) {\n try { reg?.(container) } catch {}\n }\n // Core bootstrap (cache, event bus, encryption subscriber/KMS, module subscribers)\n // Phase 5 \u2014 process-scoped once-guard. The first request runs the full\n // bootstrap() body; later requests re-register the cached services\n // directly on this request's container without re-importing or\n // re-initializing anything. HMR clears the cache (see\n // registerDiRegistrars). Skippable if a caller already wired eventBus.\n const alreadyBootstrappedOnThisContainer = !!container.registrations?.eventBus\n if (!alreadyBootstrappedOnThisContainer) {\n const cached = getBootstrapCache()\n if (cached) {\n const replay: Record<string, any> = {}\n for (const [key, value] of Object.entries(cached)) {\n if (value !== undefined && value !== null) replay[key] = asValue(value)\n }\n if (Object.keys(replay).length > 0) container.register(replay)\n } else {\n try {\n const { bootstrap } = await import('@open-mercato/core/bootstrap') as any\n if (bootstrap && typeof bootstrap === 'function') {\n await bootstrap(container)\n setBootstrapCache(harvestBootstrapCache(container))\n }\n } catch { /* optional */ }\n }\n }\n // App-level DI override (last chance)\n // This import path resolves only in the app context, not in packages\n try {\n // @ts-ignore - @/di only exists in app context, not in packages\n const appDi = await import('@/di') as any\n if (appDi?.register) {\n try {\n const maybe = appDi.register(container)\n if (maybe && typeof maybe.then === 'function') await maybe\n } catch {}\n }\n } catch {}\n applyDiOverridesToContainer({\n register: (registrations) => container.register(toAwilixRegistrations(registrations)),\n unregister: (key) => container.register({ [key]: asValue(undefined) }),\n })\n // Ensure tenant encryption subscriber is always registered on the fresh request-scoped EM\n // Phase 5 \u2014 cache `tenantEncryptionService.isEnabled()` for the process\n // lifetime. The result depends only on config that does not change at\n // runtime, so reading it once skips a config lookup per request.\n try {\n const emForEnc = container.resolve('em') as any\n const tenantEncryptionService = container.hasRegistration('tenantEncryptionService')\n ? (container.resolve('tenantEncryptionService') as any)\n : null\n if (emForEnc && tenantEncryptionService && getCachedEncryptionEnabled(tenantEncryptionService) === true) {\n const { registerTenantEncryptionSubscriber } = await import('@open-mercato/shared/lib/encryption/subscriber')\n registerTenantEncryptionSubscriber(emForEnc, tenantEncryptionService)\n }\n } catch {\n // best-effort; do not block container creation\n }\n return container\n}\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n require('server-only')\n} catch {\n // allow CLI/generator usage where Next server-only is not present\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import { asFunction, createContainer, asValue, AwilixContainer, InjectionMode, type Resolver } from 'awilix'\nimport { RequestContext } from '@mikro-orm/core'\nimport { getOrm } from '@open-mercato/shared/lib/db/mikro'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { BasicQueryEngine } from '@open-mercato/shared/lib/query/engine'\nimport { DefaultDataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { commandRegistry, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { applyDiOverridesToContainer } from '@open-mercato/shared/modules/overrides'\nimport { createOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport { createCommandOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'di' })\n\ntype DynamicCradle = Record<string, any>\n\nexport type AppContainer = AwilixContainer<DynamicCradle>\nexport type DiRegistrar = (container: AppContainer) => void\nexport type AppDiRegistrar = (container: AppContainer) => void | Promise<void>\n\n// Registration pattern for publishable packages\n// Use globalThis to survive tsx/esbuild module duplication issue where the same\n// file can be loaded as multiple module instances when mixing dynamic and static imports\nconst GLOBAL_KEY = '__openMercatoDiRegistrars__'\nconst APP_DI_REGISTRAR_KEY = '__openMercatoAppDiRegistrar__'\n// Phase 5 \u2014 process-scoped bootstrap cache. The cache/event-bus/encryption\n// services bootstrap() creates are inherently process-scoped (they hold\n// state across requests). Caching them on globalThis after the first\n// successful bootstrap call lets every subsequent request skip the\n// `await bootstrap(container)` body and just re-register the cached\n// instances. Same globalThis pattern as registerDiRegistrars so HMR\n// keeps working.\nconst BOOTSTRAP_CACHE_KEY = '__openMercatoBootstrapCache__'\nconst ENCRYPTION_ENABLED_KEY = '__openMercatoEncryptionEnabledCache__'\n\nconst BOOTSTRAP_CACHE_KEYS = [\n 'cache',\n 'eventBus',\n 'kmsService',\n 'tenantEncryptionService',\n 'rateLimiterService',\n 'searchModuleConfigs',\n 'searchIndexer',\n] as const\n\ntype BootstrapCacheEntry = Partial<Record<(typeof BOOTSTRAP_CACHE_KEYS)[number], unknown>>\n\n// Phase 5 is opt-in. Some bootstrap services close over per-request state\n// (e.g. tenantEncryptionService captures the first request's `em.fork`, the\n// event-bus's resolver closes over the first container) so naively replaying\n// them on later requests yields stale references \u2014 observed as a 500 from\n// CRUD list endpoints in `next start`. Default OFF preserves develop's\n// per-request bootstrap. Set `OM_BOOTSTRAP_CACHE=1` to opt in once each\n// cached service is verified safe for cross-request reuse.\nfunction isBootstrapCacheEnabled(): boolean {\n const raw = process.env.OM_BOOTSTRAP_CACHE\n if (raw === undefined) return false\n const normalized = raw.trim().toLowerCase()\n if (!normalized.length) return false\n if (normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'no') return false\n return true\n}\n\nfunction getBootstrapCache(): BootstrapCacheEntry | null {\n if (!isBootstrapCacheEnabled()) return null\n const existing = (globalThis as any)[BOOTSTRAP_CACHE_KEY]\n return existing && typeof existing === 'object' ? (existing as BootstrapCacheEntry) : null\n}\n\nfunction setBootstrapCache(entry: BootstrapCacheEntry): void {\n if (!isBootstrapCacheEnabled()) return\n ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = entry\n}\n\nfunction harvestBootstrapCache(container: AwilixContainer): BootstrapCacheEntry {\n const entry: BootstrapCacheEntry = {}\n for (const key of BOOTSTRAP_CACHE_KEYS) {\n try {\n const value: unknown = container.resolve(key as never)\n if (value !== undefined && value !== null) entry[key] = value\n } catch {\n // not registered \u2014 skip\n }\n }\n return entry\n}\n\ntype EncryptionEnabledProbe = { isEnabled?: () => boolean } | null | undefined\n\nfunction getCachedEncryptionEnabled(service: EncryptionEnabledProbe): boolean | null {\n if (!service || typeof service.isEnabled !== 'function') return false\n const cached = (globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY]\n if (typeof cached === 'boolean') return cached\n try {\n const result = !!service.isEnabled()\n ;(globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY] = result\n return result\n } catch {\n return null\n }\n}\n\nfunction getGlobalRegistrars(): DiRegistrar[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalRegistrars(registrars: DiRegistrar[]): void {\n (globalThis as any)[GLOBAL_KEY] = registrars\n}\n\nexport function registerDiRegistrars(registrars: DiRegistrar[]) {\n const existing = getGlobalRegistrars()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n logger.debug('DI registrars re-registered (this may occur during HMR)')\n }\n setGlobalRegistrars(registrars)\n // Force re-bootstrap on HMR \u2014 module subscribers may have changed.\n ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = null\n ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined\n}\n\nexport function getDiRegistrars(): DiRegistrar[] {\n const registrars = getGlobalRegistrars()\n if (!registrars) {\n throw new Error('[Bootstrap] DI registrars not registered. Call registerDiRegistrars() at bootstrap.')\n }\n return registrars\n}\n\nfunction getAppDiRegistrar(): AppDiRegistrar | null {\n const registrar = (globalThis as Record<string, unknown>)[APP_DI_REGISTRAR_KEY]\n return typeof registrar === 'function' ? registrar as AppDiRegistrar : null\n}\n\nexport function registerAppDiRegistrar(registrar: AppDiRegistrar | null): void {\n ;(globalThis as Record<string, unknown>)[APP_DI_REGISTRAR_KEY] = registrar\n}\n\n/** Test-only helper to drop the process-scoped bootstrap cache. */\nexport function resetBootstrapCache(): void {\n (globalThis as any)[BOOTSTRAP_CACHE_KEY] = null\n ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined\n}\n\nfunction isAwilixResolver(value: unknown): value is Resolver<unknown> {\n return Boolean(value && typeof value === 'object' && typeof (value as { resolve?: unknown }).resolve === 'function')\n}\n\nfunction toAwilixRegistrations(registrations: Record<string, unknown>): Record<string, Resolver<any>> {\n return Object.fromEntries(\n Object.entries(registrations).map(([key, value]) => [\n key,\n isAwilixResolver(value) ? value : asValue(value),\n ]),\n )\n}\n\nexport async function createRequestContainer(): Promise<AppContainer> {\n const diRegistrars = getDiRegistrars()\n const orm = await getOrm()\n // Use a fresh event manager so request-level subscribers (e.g., encryption) don't pile up globally\n const baseEm = (RequestContext.getEntityManager() as any) ?? orm.em\n const em = baseEm.fork({ clear: true, freshEventManager: true, useContext: true }) as unknown as EntityManager\n const container = createContainer<DynamicCradle>({ injectionMode: InjectionMode.CLASSIC })\n // Core registrations\n container.register({\n em: asValue(em),\n queryEngine: asValue(new BasicQueryEngine(em, undefined, () => {\n try { return container.resolve('tenantEncryptionService') as any } catch { return null }\n })),\n dataEngine: asValue(new DefaultDataEngine(em, container as any)),\n commandRegistry: asValue(commandRegistry),\n commandBus: asValue(new CommandBus()),\n // Default OSS optimistic-lock guard. Reads from the global reader store\n // (populated by `makeCrudRoute` auto-registration + any module-DI\n // hand-wired calls to `registerOptimisticLockReaders`). Service is\n // strictly additive: when `OM_OPTIMISTIC_LOCK=off` (or no header is\n // sent) it short-circuits at validateMutation. Module-level di.ts\n // registrations override this default via Awilix replace semantics \u2014\n // see the enterprise `record_locks` module for the canonical override.\n // Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md\n crudMutationGuardService: asFunction((em: EntityManager) =>\n createOptimisticLockGuardService({\n getEm: () => em,\n readers: getAllOptimisticLockReaders(),\n }),\n ).scoped(),\n // Default OSS command-level optimistic-lock guard, awaited by\n // `enforceCommandOptimisticLockWithGuards` for Command-pattern writes.\n // Header/explicit-token compare only (no `resolveExpected`), so it is\n // behaviourally identical to calling `enforceCommandOptimisticLock`\n // directly. The enterprise `record_locks` module overrides this DI key\n // with a lock-backed `resolveExpected` via Awilix replace semantics.\n // Spec: .ai/specs/enterprise/2026-06-09-record-locks-unified-coverage.md (Phase 0)\n commandOptimisticLockGuardService: asFunction(() =>\n createCommandOptimisticLockGuardService(),\n ).scoped(),\n })\n // Allow modules to override/extend\n for (const reg of diRegistrars) {\n try { reg?.(container) } catch {}\n }\n // Core bootstrap (cache, event bus, encryption subscriber/KMS, module subscribers)\n // Phase 5 \u2014 process-scoped once-guard. The first request runs the full\n // bootstrap() body; later requests re-register the cached services\n // directly on this request's container without re-importing or\n // re-initializing anything. HMR clears the cache (see\n // registerDiRegistrars). Skippable if a caller already wired eventBus.\n const alreadyBootstrappedOnThisContainer = !!container.registrations?.eventBus\n if (!alreadyBootstrappedOnThisContainer) {\n const cached = getBootstrapCache()\n if (cached) {\n const replay: Record<string, any> = {}\n for (const [key, value] of Object.entries(cached)) {\n if (value !== undefined && value !== null) replay[key] = asValue(value)\n }\n if (Object.keys(replay).length > 0) container.register(replay)\n } else {\n try {\n const { bootstrap } = await import('@open-mercato/core/bootstrap') as any\n if (bootstrap && typeof bootstrap === 'function') {\n await bootstrap(container)\n setBootstrapCache(harvestBootstrapCache(container))\n }\n } catch { /* optional */ }\n }\n }\n // App-level DI override. Scaffolded apps register this callback explicitly\n // from their own bootstrap module so app aliases resolve in app context.\n const appDiRegistrar = getAppDiRegistrar()\n if (appDiRegistrar) {\n try {\n await appDiRegistrar(container)\n } catch (error) {\n logger.error('App-level DI registrar failed', { err: error })\n }\n } else {\n // Backward-compatible fallback for apps that have not adopted explicit wiring.\n try {\n // @ts-ignore - @/di only exists in app context, not in packages\n const appDi = await import('@/di') as any\n if (appDi?.register) {\n try {\n const maybe = appDi.register(container)\n if (maybe && typeof maybe.then === 'function') await maybe\n } catch (error) {\n logger.error('App-level DI registrar failed', { err: error })\n }\n }\n } catch {}\n }\n applyDiOverridesToContainer({\n register: (registrations) => container.register(toAwilixRegistrations(registrations)),\n unregister: (key) => container.register({ [key]: asValue(undefined) }),\n })\n // Ensure tenant encryption subscriber is always registered on the fresh request-scoped EM\n // Phase 5 \u2014 cache `tenantEncryptionService.isEnabled()` for the process\n // lifetime. The result depends only on config that does not change at\n // runtime, so reading it once skips a config lookup per request.\n try {\n const emForEnc = container.resolve('em') as any\n const tenantEncryptionService = container.hasRegistration('tenantEncryptionService')\n ? (container.resolve('tenantEncryptionService') as any)\n : null\n if (emForEnc && tenantEncryptionService && getCachedEncryptionEnabled(tenantEncryptionService) === true) {\n const { registerTenantEncryptionSubscriber } = await import('@open-mercato/shared/lib/encryption/subscriber')\n registerTenantEncryptionSubscriber(emForEnc, tenantEncryptionService)\n }\n } catch {\n // best-effort; do not block container creation\n }\n return container\n}\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n require('server-only')\n} catch {\n // allow CLI/generator usage where Next server-only is not present\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["em"]
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.7-develop.6630.1.c8a615e536";
1
+ const APP_VERSION = "0.6.7-develop.6645.1.52f0b37813";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6630.1.c8a615e536'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6645.1.52f0b37813'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
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.6630.1.c8a615e536",
3
+ "version": "0.6.7-develop.6645.1.52f0b37813",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -97,7 +97,7 @@
97
97
  "@mikro-orm/core": "^7.1.5",
98
98
  "@mikro-orm/decorators": "^7.1.5",
99
99
  "@mikro-orm/postgresql": "^7.1.5",
100
- "@open-mercato/cache": "0.6.7-develop.6630.1.c8a615e536",
100
+ "@open-mercato/cache": "0.6.7-develop.6645.1.52f0b37813",
101
101
  "@types/sanitize-html": "^2.16.1",
102
102
  "dotenv": "^17.4.2",
103
103
  "pino": "^10.3.1",
@@ -1,6 +1,6 @@
1
1
  import type { BootstrapData, BootstrapOptions } from './types'
2
2
  import { registerOrmEntities } from '../db/mikro'
3
- import { registerDiRegistrars } from '../di/container'
3
+ import { registerAppDiRegistrar, registerDiRegistrars } from '../di/container'
4
4
  import { registerModules } from '../modules/registry'
5
5
  import { registerEntityIds } from '../encryption/entityIds'
6
6
  import { registerEntityFields } from '../encryption/entityFields'
@@ -34,6 +34,9 @@ let _asyncRegistrationPromise: Promise<void> | null = null
34
34
  */
35
35
  export function createBootstrap(data: BootstrapData, options: BootstrapOptions = {}) {
36
36
  return function bootstrap(): void {
37
+ if (options.appDiRegistrar) {
38
+ registerAppDiRegistrar(options.appDiRegistrar)
39
+ }
37
40
  // In development, always re-run registrations to handle HMR
38
41
  // (Module state may be reset when Turbopack reloads packages)
39
42
  if (_bootstrapped && process.env.NODE_ENV !== 'development') return
@@ -1,4 +1,4 @@
1
- import type { DiRegistrar } from '../di/container'
1
+ import type { AppDiRegistrar, DiRegistrar } from '../di/container'
2
2
  import type { EntityIds } from '../encryption/entityIds'
3
3
  import type { EntityFieldsRegistry } from '../encryption/entityFields'
4
4
  import type { Module, ModuleDashboardWidgetEntry, ModuleInjectionWidgetEntry } from '../../modules/registry'
@@ -70,4 +70,5 @@ export interface BootstrapData {
70
70
  export interface BootstrapOptions {
71
71
  skipSearchConfigs?: boolean
72
72
  onRegistrationComplete?: () => void
73
+ appDiRegistrar?: AppDiRegistrar
73
74
  }
@@ -1,6 +1,6 @@
1
1
  import type { AwilixContainer } from 'awilix'
2
2
  import { registerMutationGuards } from '../mutation-guard-store'
3
- import type { MutationGuard } from '../mutation-guard-registry'
3
+ import { bridgeLegacyGuard, type MutationGuard } from '../mutation-guard-registry'
4
4
  import { runRouteMutationGuards, toRegistryMutationOperation } from '../route-mutation-guard'
5
5
  import { createLogger } from '@open-mercato/shared/lib/logger'
6
6
 
@@ -16,12 +16,13 @@ jest.mock('@open-mercato/shared/lib/logger', () => {
16
16
  return { createLogger: jest.fn(() => mocked) }
17
17
  })
18
18
  const loggerError = createLogger('shared').error as jest.Mock
19
-
19
+ const loggerWarn = createLogger('shared').warn as jest.Mock
20
20
 
21
21
  type Registrations = Record<string, unknown>
22
22
 
23
23
  function makeContainer(registrations: Registrations = {}): AwilixContainer {
24
24
  return {
25
+ hasRegistration: (name: string) => Object.prototype.hasOwnProperty.call(registrations, name),
25
26
  resolve: (name: string) => {
26
27
  if (Object.prototype.hasOwnProperty.call(registrations, name)) return registrations[name]
27
28
  throw new Error(`[test] no registration for ${name}`)
@@ -272,4 +273,24 @@ describe('runRouteMutationGuards', () => {
272
273
  expect(result.errorStatus).toBe(409)
273
274
  expect(result.errorBody).toEqual({ error: 'locked' })
274
275
  })
276
+
277
+ it('warns only once when the registered legacy guard cannot be resolved', () => {
278
+ delete (globalThis as Record<string, unknown>).__openMercatoCrudMutationGuardResolutionWarningEmitted__
279
+ loggerWarn.mockClear()
280
+ const resolutionError = new Error('Could not resolve scopedEm')
281
+ const container = {
282
+ hasRegistration: () => true,
283
+ resolve: () => {
284
+ throw resolutionError
285
+ },
286
+ } as unknown as AwilixContainer
287
+
288
+ expect(bridgeLegacyGuard(container)).toBeNull()
289
+ expect(bridgeLegacyGuard(container)).toBeNull()
290
+ expect(loggerWarn).toHaveBeenCalledTimes(1)
291
+ expect(loggerWarn).toHaveBeenCalledWith(
292
+ 'CRUD mutation guard service could not be resolved; the legacy guard bridge is disabled',
293
+ { err: resolutionError },
294
+ )
295
+ })
275
296
  })
@@ -1,5 +1,6 @@
1
1
  import type { AwilixContainer } from 'awilix'
2
2
  import { hasAllFeatures } from '../../security/features'
3
+ import { resolveCrudMutationGuardService } from './mutation-guard-service'
3
4
 
4
5
  // ---------------------------------------------------------------------------
5
6
  // Types
@@ -125,45 +126,8 @@ export async function runMutationGuards(
125
126
  // Legacy guard bridge
126
127
  // ---------------------------------------------------------------------------
127
128
 
128
- type LegacyCrudMutationGuardService = {
129
- validateMutation: (input: {
130
- tenantId: string
131
- organizationId?: string | null
132
- userId: string
133
- resourceKind: string
134
- resourceId: string
135
- operation: 'create' | 'update' | 'delete' | 'custom'
136
- requestMethod: string
137
- requestHeaders: Headers
138
- mutationPayload?: Record<string, unknown> | null
139
- }) => Promise<{ ok: boolean; status?: number; body?: Record<string, unknown>; shouldRunAfterSuccess?: boolean; metadata?: Record<string, unknown> | null } | null>
140
- afterMutationSuccess: (input: {
141
- tenantId: string
142
- organizationId?: string | null
143
- userId: string
144
- resourceKind: string
145
- resourceId: string
146
- operation: 'create' | 'update' | 'delete' | 'custom'
147
- requestMethod: string
148
- requestHeaders: Headers
149
- metadata?: Record<string, unknown> | null
150
- }) => Promise<void>
151
- }
152
-
153
- function resolveLegacyGuardService(container: AwilixContainer): LegacyCrudMutationGuardService | null {
154
- try {
155
- const service = container.resolve<LegacyCrudMutationGuardService>('crudMutationGuardService')
156
- if (!service) return null
157
- if (typeof service.validateMutation !== 'function') return null
158
- if (typeof service.afterMutationSuccess !== 'function') return null
159
- return service
160
- } catch {
161
- return null
162
- }
163
- }
164
-
165
129
  export function bridgeLegacyGuard(container: AwilixContainer): MutationGuard | null {
166
- const legacyService = resolveLegacyGuardService(container)
130
+ const legacyService = resolveCrudMutationGuardService(container)
167
131
  if (!legacyService) return null
168
132
 
169
133
  return {
@@ -0,0 +1,48 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import { createLogger } from '../logger'
3
+ import type {
4
+ CrudMutationGuardAfterSuccessInput,
5
+ CrudMutationGuardValidateInput,
6
+ CrudMutationGuardValidationResult,
7
+ } from './mutation-guard'
8
+
9
+ const logger = createLogger('shared').child({ component: 'crud' })
10
+ const RESOLUTION_WARNING_KEY = '__openMercatoCrudMutationGuardResolutionWarningEmitted__'
11
+
12
+ export type CrudMutationGuardServiceLike = {
13
+ validateMutation: (
14
+ input: CrudMutationGuardValidateInput,
15
+ ) => Promise<CrudMutationGuardValidationResult | null>
16
+ afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>
17
+ }
18
+
19
+ function warnResolutionFailureOnce(error: unknown): void {
20
+ const globalScope = globalThis as Record<string, unknown>
21
+ if (globalScope[RESOLUTION_WARNING_KEY] === true) return
22
+ globalScope[RESOLUTION_WARNING_KEY] = true
23
+ logger.warn('CRUD mutation guard service could not be resolved; the legacy guard bridge is disabled', {
24
+ err: error,
25
+ })
26
+ }
27
+
28
+ export function resolveCrudMutationGuardService(
29
+ container: AwilixContainer,
30
+ ): CrudMutationGuardServiceLike | null {
31
+ if (
32
+ typeof container.hasRegistration === 'function'
33
+ && !container.hasRegistration('crudMutationGuardService')
34
+ ) {
35
+ return null
36
+ }
37
+
38
+ try {
39
+ const service = container.resolve<CrudMutationGuardServiceLike>('crudMutationGuardService')
40
+ if (!service) return null
41
+ if (typeof service.validateMutation !== 'function') return null
42
+ if (typeof service.afterMutationSuccess !== 'function') return null
43
+ return service
44
+ } catch (error) {
45
+ warnResolutionFailureOnce(error)
46
+ return null
47
+ }
48
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AwilixContainer } from 'awilix'
2
2
  import { createLogger } from '../logger'
3
+ import { resolveCrudMutationGuardService } from './mutation-guard-service'
3
4
 
4
5
  const logger = createLogger('shared').child({ component: 'crud' })
5
6
 
@@ -43,23 +44,6 @@ export type CrudMutationGuardAfterSuccessInput = {
43
44
  metadata?: Record<string, unknown> | null
44
45
  }
45
46
 
46
- type CrudMutationGuardServiceLike = {
47
- validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>
48
- afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>
49
- }
50
-
51
- function resolveCrudMutationGuardService(container: AwilixContainer): CrudMutationGuardServiceLike | null {
52
- try {
53
- const service = container.resolve<CrudMutationGuardServiceLike>('crudMutationGuardService')
54
- if (!service) return null
55
- if (typeof service.validateMutation !== 'function') return null
56
- if (typeof service.afterMutationSuccess !== 'function') return null
57
- return service
58
- } catch {
59
- return null
60
- }
61
- }
62
-
63
47
  /**
64
48
  * @deprecated Resolves ONLY the single DI-registered `crudMutationGuardService`,
65
49
  * so it silently bypasses every guard in the global mutation-guard store
@@ -8,6 +8,7 @@
8
8
  // `OM_BOOTSTRAP_CACHE` gates the whole behavior; default OFF.
9
9
 
10
10
  import { asValue } from 'awilix'
11
+ import type { AwilixContainer } from 'awilix'
11
12
 
12
13
  // Mock the deep ORM/engine imports BEFORE importing container.ts so we can
13
14
  // exercise the bootstrap once-guard without pulling in MikroORM decorators.
@@ -64,11 +65,14 @@ jest.mock(
64
65
  () => ({
65
66
  __esModule: true,
66
67
  applyDiOverridesToContainer: () => {},
68
+ applyModuleOverridesToModules: (modules: unknown[]) => modules,
69
+ applyComponentOverridesToEntries: (entries: unknown[]) => entries,
67
70
  }),
68
71
  { virtual: false },
69
72
  )
70
73
 
71
74
  const {
75
+ registerAppDiRegistrar,
72
76
  registerDiRegistrars,
73
77
  resetBootstrapCache,
74
78
  } = require('@open-mercato/shared/lib/di/container')
@@ -105,6 +109,7 @@ const ORIGINAL_FLAG = process.env.OM_BOOTSTRAP_CACHE
105
109
  describe('bootstrap once-guard cache', () => {
106
110
  beforeEach(() => {
107
111
  resetBootstrapCache()
112
+ registerAppDiRegistrar(null)
108
113
  bootstrapMock.mockClear()
109
114
  subscriberRegistered.mockClear()
110
115
  registerDiRegistrars([])
@@ -146,6 +151,55 @@ describe('bootstrap once-guard cache', () => {
146
151
  expect(bootstrapMock).toHaveBeenCalledTimes(2)
147
152
  })
148
153
 
154
+ it('resolves the default optimistic-lock guard in CLASSIC injection mode', async () => {
155
+ const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
156
+ const container = await createRequestContainer()
157
+
158
+ expect(container.resolve('crudMutationGuardService')).toEqual(expect.objectContaining({
159
+ validateMutation: expect.any(Function),
160
+ afterMutationSuccess: expect.any(Function),
161
+ }))
162
+ })
163
+
164
+ it('runs the explicitly registered app DI registrar for each request container', async () => {
165
+ const appDiRegistrar = jest.fn(async (container: AwilixContainer) => {
166
+ container.register({ appLevelService: asValue('app-level') })
167
+ })
168
+ registerAppDiRegistrar(appDiRegistrar)
169
+
170
+ const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
171
+ const container = await createRequestContainer()
172
+
173
+ expect(appDiRegistrar).toHaveBeenCalledTimes(1)
174
+ expect(appDiRegistrar).toHaveBeenCalledWith(container)
175
+ expect(container.resolve('appLevelService')).toBe('app-level')
176
+ })
177
+
178
+ it('preserves the app DI registrar when a CLI-style bootstrap omits the option', async () => {
179
+ const appDiRegistrar = jest.fn((container: AwilixContainer) => {
180
+ container.register({ appLevelService: asValue('preserved') })
181
+ })
182
+ registerAppDiRegistrar(appDiRegistrar)
183
+
184
+ const { createBootstrap, resetBootstrapState } = await import('@open-mercato/shared/lib/bootstrap/factory')
185
+ resetBootstrapState()
186
+ createBootstrap({
187
+ modules: [],
188
+ entities: [],
189
+ diRegistrars: [],
190
+ entityIds: {},
191
+ dashboardWidgetEntries: [],
192
+ injectionWidgetEntries: [],
193
+ injectionTables: [],
194
+ })()
195
+
196
+ const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
197
+ const container = await createRequestContainer()
198
+
199
+ expect(appDiRegistrar).toHaveBeenCalledTimes(1)
200
+ expect(container.resolve('appLevelService')).toBe('preserved')
201
+ })
202
+
149
203
  it('registerDiRegistrars clears the cache so HMR re-runs bootstrap on the next request', async () => {
150
204
  process.env.OM_BOOTSTRAP_CACHE = '1'
151
205
  const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
@@ -17,11 +17,13 @@ type DynamicCradle = Record<string, any>
17
17
 
18
18
  export type AppContainer = AwilixContainer<DynamicCradle>
19
19
  export type DiRegistrar = (container: AppContainer) => void
20
+ export type AppDiRegistrar = (container: AppContainer) => void | Promise<void>
20
21
 
21
22
  // Registration pattern for publishable packages
22
23
  // Use globalThis to survive tsx/esbuild module duplication issue where the same
23
24
  // file can be loaded as multiple module instances when mixing dynamic and static imports
24
25
  const GLOBAL_KEY = '__openMercatoDiRegistrars__'
26
+ const APP_DI_REGISTRAR_KEY = '__openMercatoAppDiRegistrar__'
25
27
  // Phase 5 — process-scoped bootstrap cache. The cache/event-bus/encryption
26
28
  // services bootstrap() creates are inherently process-scoped (they hold
27
29
  // state across requests). Caching them on globalThis after the first
@@ -126,6 +128,15 @@ export function getDiRegistrars(): DiRegistrar[] {
126
128
  return registrars
127
129
  }
128
130
 
131
+ function getAppDiRegistrar(): AppDiRegistrar | null {
132
+ const registrar = (globalThis as Record<string, unknown>)[APP_DI_REGISTRAR_KEY]
133
+ return typeof registrar === 'function' ? registrar as AppDiRegistrar : null
134
+ }
135
+
136
+ export function registerAppDiRegistrar(registrar: AppDiRegistrar | null): void {
137
+ ;(globalThis as Record<string, unknown>)[APP_DI_REGISTRAR_KEY] = registrar
138
+ }
139
+
129
140
  /** Test-only helper to drop the process-scoped bootstrap cache. */
130
141
  export function resetBootstrapCache(): void {
131
142
  (globalThis as any)[BOOTSTRAP_CACHE_KEY] = null
@@ -169,9 +180,9 @@ export async function createRequestContainer(): Promise<AppContainer> {
169
180
  // registrations override this default via Awilix replace semantics —
170
181
  // see the enterprise `record_locks` module for the canonical override.
171
182
  // Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md
172
- crudMutationGuardService: asFunction(({ em: scopedEm }: { em: EntityManager }) =>
183
+ crudMutationGuardService: asFunction((em: EntityManager) =>
173
184
  createOptimisticLockGuardService({
174
- getEm: () => scopedEm,
185
+ getEm: () => em,
175
186
  readers: getAllOptimisticLockReaders(),
176
187
  }),
177
188
  ).scoped(),
@@ -215,18 +226,30 @@ export async function createRequestContainer(): Promise<AppContainer> {
215
226
  } catch { /* optional */ }
216
227
  }
217
228
  }
218
- // App-level DI override (last chance)
219
- // This import path resolves only in the app context, not in packages
220
- try {
221
- // @ts-ignore - @/di only exists in app context, not in packages
222
- const appDi = await import('@/di') as any
223
- if (appDi?.register) {
224
- try {
225
- const maybe = appDi.register(container)
226
- if (maybe && typeof maybe.then === 'function') await maybe
227
- } catch {}
229
+ // App-level DI override. Scaffolded apps register this callback explicitly
230
+ // from their own bootstrap module so app aliases resolve in app context.
231
+ const appDiRegistrar = getAppDiRegistrar()
232
+ if (appDiRegistrar) {
233
+ try {
234
+ await appDiRegistrar(container)
235
+ } catch (error) {
236
+ logger.error('App-level DI registrar failed', { err: error })
228
237
  }
229
- } catch {}
238
+ } else {
239
+ // Backward-compatible fallback for apps that have not adopted explicit wiring.
240
+ try {
241
+ // @ts-ignore - @/di only exists in app context, not in packages
242
+ const appDi = await import('@/di') as any
243
+ if (appDi?.register) {
244
+ try {
245
+ const maybe = appDi.register(container)
246
+ if (maybe && typeof maybe.then === 'function') await maybe
247
+ } catch (error) {
248
+ logger.error('App-level DI registrar failed', { err: error })
249
+ }
250
+ }
251
+ } catch {}
252
+ }
230
253
  applyDiOverridesToContainer({
231
254
  register: (registrations) => container.register(toAwilixRegistrations(registrations)),
232
255
  unregister: (key) => container.register({ [key]: asValue(undefined) }),