@open-mercato/shared 0.6.5-canary.4425.1.75a266b232 → 0.6.5-develop.4393.1.de282b5dfd

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,12 +1,10 @@
1
- import { asFunction, createContainer, asValue, InjectionMode } from "awilix";
1
+ import { createContainer, asValue, InjectionMode } from "awilix";
2
2
  import { RequestContext } from "@mikro-orm/core";
3
3
  import { getOrm } from "@open-mercato/shared/lib/db/mikro";
4
4
  import { BasicQueryEngine } from "@open-mercato/shared/lib/query/engine";
5
5
  import { DefaultDataEngine } from "@open-mercato/shared/lib/data/engine";
6
6
  import { commandRegistry, CommandBus } from "@open-mercato/shared/lib/commands";
7
7
  import { applyDiOverridesToContainer } from "@open-mercato/shared/modules/overrides";
8
- import { createOptimisticLockGuardService } from "@open-mercato/shared/lib/crud/optimistic-lock";
9
- import { getAllOptimisticLockReaders } from "@open-mercato/shared/lib/crud/optimistic-lock-store";
10
8
  const GLOBAL_KEY = "__openMercatoDiRegistrars__";
11
9
  const BOOTSTRAP_CACHE_KEY = "__openMercatoBootstrapCache__";
12
10
  const ENCRYPTION_ENABLED_KEY = "__openMercatoEncryptionEnabledCache__";
@@ -113,21 +111,7 @@ async function createRequestContainer() {
113
111
  })),
114
112
  dataEngine: asValue(new DefaultDataEngine(em, container)),
115
113
  commandRegistry: asValue(commandRegistry),
116
- commandBus: asValue(new CommandBus()),
117
- // Default OSS optimistic-lock guard. Reads from the global reader store
118
- // (populated by `makeCrudRoute` auto-registration + any module-DI
119
- // hand-wired calls to `registerOptimisticLockReaders`). Service is
120
- // strictly additive: when `OM_OPTIMISTIC_LOCK=off` (or no header is
121
- // sent) it short-circuits at validateMutation. Module-level di.ts
122
- // registrations override this default via Awilix replace semantics —
123
- // see the enterprise `record_locks` module for the canonical override.
124
- // Spec: .ai/specs/2026-05-25-oss-optimistic-locking.md
125
- crudMutationGuardService: asFunction(
126
- ({ em: scopedEm }) => createOptimisticLockGuardService({
127
- getEm: () => scopedEm,
128
- readers: getAllOptimisticLockReaders()
129
- })
130
- ).scoped()
114
+ commandBus: asValue(new CommandBus())
131
115
  });
132
116
  for (const reg of diRegistrars) {
133
117
  try {
@@ -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'\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 console.debug('[Bootstrap] 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/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 })\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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",
4
+ "sourcesContent": ["import { 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'\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 console.debug('[Bootstrap] 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 })\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
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.5-canary.4425.1.75a266b232";
1
+ const APP_VERSION = "0.6.5-develop.4393.1.de282b5dfd";
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.5-canary.4425.1.75a266b232'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.5-develop.4393.1.de282b5dfd'\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.5-canary.4425.1.75a266b232",
3
+ "version": "0.6.5-develop.4393.1.de282b5dfd",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -92,7 +92,7 @@
92
92
  "@mikro-orm/core": "^7.1.3",
93
93
  "@mikro-orm/decorators": "^7.1.3",
94
94
  "@mikro-orm/postgresql": "^7.1.3",
95
- "@open-mercato/cache": "0.6.5-canary.4425.1.75a266b232",
95
+ "@open-mercato/cache": "0.6.5-develop.4393.1.de282b5dfd",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.1.0",
98
98
  "re2js": "2.8.3",
@@ -4,11 +4,6 @@ jest.mock('@open-mercato/cache', () => ({
4
4
 
5
5
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
6
6
  import { registerApiInterceptors } from '@open-mercato/shared/lib/crud/interceptor-registry'
7
- import {
8
- clearOptimisticLockReadersForTests,
9
- getAllOptimisticLockReaders,
10
- registerOptimisticLockReaders,
11
- } from '@open-mercato/shared/lib/crud/optimistic-lock-store'
12
7
  import { loadCustomFieldDefinitionIndex } from '@open-mercato/shared/lib/crud/custom-fields'
13
8
  import { z } from 'zod'
14
9
 
@@ -759,104 +754,3 @@ describe('CRUD Factory', () => {
759
754
  expect(body._interceptor).toEqual({ ok: true, count: 1 })
760
755
  })
761
756
  })
762
-
763
- describe('CRUD Factory — optimistic-lock auto-registration', () => {
764
- beforeEach(() => {
765
- clearOptimisticLockReadersForTests()
766
- })
767
-
768
- afterAll(() => {
769
- clearOptimisticLockReadersForTests()
770
- })
771
-
772
- function makeMinimalRoute(opts: { eventsResource: string; entity: any }) {
773
- return makeCrudRoute({
774
- metadata: { GET: { requireAuth: true } },
775
- orm: { entity: opts.entity, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId' },
776
- events: { module: opts.eventsResource.split('.')[0], entity: opts.eventsResource.split('.')[1], persistent: false } as any,
777
- list: { schema: z.object({}).passthrough() as any },
778
- create: {
779
- commandId: `${opts.eventsResource}.create`,
780
- schema: z.object({}).passthrough() as any,
781
- },
782
- update: {
783
- commandId: `${opts.eventsResource}.update`,
784
- schema: z.object({ id: z.string() }).passthrough() as any,
785
- },
786
- del: {
787
- commandId: `${opts.eventsResource}.delete`,
788
- schema: z.object({ id: z.string() }).passthrough() as any,
789
- },
790
- })
791
- }
792
-
793
- it('auto-registers a reader for the route resourceKind at factory call time', () => {
794
- expect(getAllOptimisticLockReaders()).toEqual({})
795
- makeMinimalRoute({ eventsResource: 'example.todo', entity: Todo })
796
- const all = getAllOptimisticLockReaders()
797
- expect(Object.keys(all)).toContain('example.todo')
798
- expect(typeof all['example.todo']).toBe('function')
799
- })
800
-
801
- it('does NOT override an existing hand-wired reader (IfAbsent semantics)', () => {
802
- const handWired = async () => 'hand-wired'
803
- registerOptimisticLockReaders({ 'example.todo': handWired })
804
- makeMinimalRoute({ eventsResource: 'example.todo', entity: Todo })
805
- expect(getAllOptimisticLockReaders()['example.todo']).toBe(handWired)
806
- })
807
-
808
- it('skips registration when the entity has no resolvable resourceKind', () => {
809
- expect(getAllOptimisticLockReaders()).toEqual({})
810
- // Route with no events.module + no command IDs → resourceKind falls back to 'resource'
811
- makeCrudRoute({
812
- metadata: { GET: { requireAuth: true } },
813
- orm: { entity: Todo },
814
- list: { schema: z.object({}).passthrough() as any },
815
- } as any)
816
- // 'resource' is filtered out by the auto-registration guard
817
- expect(getAllOptimisticLockReaders()['resource']).toBeUndefined()
818
- })
819
-
820
- it('the registered reader projects only updatedAt and fails open on schema mismatch', async () => {
821
- makeMinimalRoute({ eventsResource: 'example.todo', entity: Todo })
822
- const reader = getAllOptimisticLockReaders()['example.todo']
823
- expect(reader).toBeDefined()
824
- let captured: { entity: unknown; filter: Record<string, unknown>; options?: Record<string, unknown> } | null = null
825
- const fakeEm = {
826
- async findOne(entity: unknown, filter: Record<string, unknown>, options?: Record<string, unknown>) {
827
- captured = { entity, filter, options }
828
- return { updatedAt: new Date('2026-05-26T07:30:00.000Z') }
829
- },
830
- } as never
831
- const out = await reader!(fakeEm, {
832
- resourceKind: 'example.todo',
833
- resourceId: 'todo-1',
834
- tenantId: 'tenant-1',
835
- organizationId: 'org-1',
836
- })
837
- expect(out).toBe('2026-05-26T07:30:00.000Z')
838
- expect(captured).not.toBeNull()
839
- expect(captured!.entity).toBe(Todo)
840
- expect(captured!.filter).toEqual({
841
- id: 'todo-1',
842
- tenantId: 'tenant-1',
843
- organizationId: 'org-1',
844
- deletedAt: null,
845
- })
846
- expect(captured!.options).toEqual({ fields: ['updatedAt'] })
847
-
848
- // Fail-open contract: throwing findOne yields null, not a re-thrown error.
849
- const throwingEm = {
850
- async findOne() {
851
- throw new Error('schema mismatch')
852
- },
853
- } as never
854
- const safe = await reader!(throwingEm, {
855
- resourceKind: 'example.todo',
856
- resourceId: 'todo-1',
857
- tenantId: 'tenant-1',
858
- organizationId: 'org-1',
859
- })
860
- expect(safe).toBeNull()
861
- })
862
- })
@@ -69,8 +69,6 @@ import { runApiInterceptorsAfter, runApiInterceptorsBefore } from './interceptor
69
69
  import { mergeIdFilter, parseIdsParam } from './ids'
70
70
  import { mergeAdvancedFilters } from './advanced-filter-integration'
71
71
  import { parseExtensionHeaders } from '../umes/extension-headers'
72
- import { createGenericOptimisticLockReader } from './optimistic-lock'
73
- import { registerOptimisticLockReaderIfAbsent } from './optimistic-lock-store'
74
72
 
75
73
  type RbacServiceLike = {
76
74
  getGrantedFeatures: (userId: string, opts: { tenantId: string | null; organizationId: string | null }) => Promise<string[]>
@@ -941,27 +939,6 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
941
939
  const resourceKind = resourceInfo.primary
942
940
  const resourceAliases = resourceInfo.aliases
943
941
  const resourceTargets = expandResourceAliases(resourceKind, resourceAliases)
944
-
945
- // OSS opt-in optimistic locking — auto-register a generic reader for every
946
- // CRUD entity using the factory's own ORM config (Step 13.3 of the spec at
947
- // .ai/specs/2026-05-25-oss-optimistic-locking.md). Hand-wired readers
948
- // registered earlier via module DI (customers/sales) always win because we
949
- // use the `IfAbsent` variant. Skipped silently when the route has no
950
- // resolvable resourceKind or no ORM entity class (e.g. virtual routes).
951
- if (ormCfg.entity && resourceKind && resourceKind !== 'resource') {
952
- const genericReader = createGenericOptimisticLockReader({
953
- entity: ormCfg.entity,
954
- idField: ormCfg.idField ?? 'id',
955
- tenantField: ormCfg.tenantField,
956
- orgField: ormCfg.orgField,
957
- softDeleteField: ormCfg.softDeleteField,
958
- })
959
- const keysToRegister: Record<string, typeof genericReader> = { [resourceKind]: genericReader }
960
- for (const alias of resourceAliases) {
961
- if (alias && alias !== resourceKind) keysToRegister[alias] = genericReader
962
- }
963
- registerOptimisticLockReaderIfAbsent(keysToRegister)
964
- }
965
942
  const defaultIdentifierResolver: CrudIdentifierResolver = (entity, _action) => {
966
943
  const id = normalizeIdentifierValue((entity as any)[ormCfg.idField!])
967
944
  const orgId = ormCfg.orgField ? normalizeIdentifierValue((entity as any)[ormCfg.orgField]) : null
@@ -1,4 +1,4 @@
1
- import { asFunction, createContainer, asValue, AwilixContainer, InjectionMode, type Resolver } from 'awilix'
1
+ import { createContainer, asValue, AwilixContainer, InjectionMode, type Resolver } from 'awilix'
2
2
  import { RequestContext } from '@mikro-orm/core'
3
3
  import { getOrm } from '@open-mercato/shared/lib/db/mikro'
4
4
  import { EntityManager } from '@mikro-orm/postgresql'
@@ -6,8 +6,6 @@ import { BasicQueryEngine } from '@open-mercato/shared/lib/query/engine'
6
6
  import { DefaultDataEngine } from '@open-mercato/shared/lib/data/engine'
7
7
  import { commandRegistry, CommandBus } from '@open-mercato/shared/lib/commands'
8
8
  import { applyDiOverridesToContainer } from '@open-mercato/shared/modules/overrides'
9
- import { createOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock'
10
- import { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'
11
9
 
12
10
  type DynamicCradle = Record<string, any>
13
11
 
@@ -157,20 +155,6 @@ export async function createRequestContainer(): Promise<AppContainer> {
157
155
  dataEngine: asValue(new DefaultDataEngine(em, container as any)),
158
156
  commandRegistry: asValue(commandRegistry),
159
157
  commandBus: asValue(new CommandBus()),
160
- // Default OSS optimistic-lock guard. Reads from the global reader store
161
- // (populated by `makeCrudRoute` auto-registration + any module-DI
162
- // hand-wired calls to `registerOptimisticLockReaders`). Service is
163
- // strictly additive: when `OM_OPTIMISTIC_LOCK=off` (or no header is
164
- // sent) it short-circuits at validateMutation. Module-level di.ts
165
- // registrations override this default via Awilix replace semantics —
166
- // see the enterprise `record_locks` module for the canonical override.
167
- // Spec: .ai/specs/2026-05-25-oss-optimistic-locking.md
168
- crudMutationGuardService: asFunction(({ em: scopedEm }: { em: EntityManager }) =>
169
- createOptimisticLockGuardService({
170
- getEm: () => scopedEm,
171
- readers: getAllOptimisticLockReaders(),
172
- }),
173
- ).scoped(),
174
158
  })
175
159
  // Allow modules to override/extend
176
160
  for (const reg of diRegistrars) {
@@ -1,109 +0,0 @@
1
- import { CrudHttpError } from "./errors.js";
2
- import {
3
- OPTIMISTIC_LOCK_CONFLICT_CODE,
4
- OPTIMISTIC_LOCK_CONFLICT_ERROR,
5
- OPTIMISTIC_LOCK_ENV_VAR,
6
- OPTIMISTIC_LOCK_HEADER_NAME
7
- } from "./optimistic-lock-headers.js";
8
- import {
9
- normalizeIsoToken,
10
- parseOptimisticLockEnv
11
- } from "./optimistic-lock.js";
12
- function toIsoOrNull(value) {
13
- if (value == null) return null;
14
- if (value instanceof Date) {
15
- const ms = value.getTime();
16
- return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
17
- }
18
- const trimmed = String(value).trim();
19
- if (!trimmed) return null;
20
- return normalizeIsoToken(trimmed);
21
- }
22
- function resolveConfig(envValue) {
23
- return parseOptimisticLockEnv(envValue !== void 0 ? envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR]);
24
- }
25
- function isResourceLockEnabled(config, resourceKind) {
26
- if (config.mode === "off") return false;
27
- if (config.mode === "all") return true;
28
- return config.entities.has(resourceKind.toLowerCase());
29
- }
30
- function readOptimisticLockExpected(source) {
31
- if (!source) return null;
32
- const headers = source instanceof Headers ? source : source.headers instanceof Headers ? source.headers : null;
33
- if (!headers) return null;
34
- const direct = headers.get(OPTIMISTIC_LOCK_HEADER_NAME);
35
- if (typeof direct === "string" && direct.trim().length > 0) return direct.trim();
36
- return null;
37
- }
38
- function buildOptimisticLockConflictBody(currentIso, expectedIso) {
39
- return {
40
- error: OPTIMISTIC_LOCK_CONFLICT_ERROR,
41
- code: OPTIMISTIC_LOCK_CONFLICT_CODE,
42
- currentUpdatedAt: currentIso,
43
- expectedUpdatedAt: expectedIso
44
- };
45
- }
46
- function assertOptimisticLock(input) {
47
- const config = resolveConfig(input.envValue);
48
- if (!isResourceLockEnabled(config, input.resourceKind)) return;
49
- const expectedIso = toIsoOrNull(input.expected);
50
- if (expectedIso == null) return;
51
- const currentIso = toIsoOrNull(input.current);
52
- if (currentIso == null) return;
53
- if (currentIso === expectedIso) return;
54
- throw new CrudHttpError(409, buildOptimisticLockConflictBody(currentIso, expectedIso));
55
- }
56
- function enforceCommandOptimisticLock(input) {
57
- const expected = input.expected !== void 0 && input.expected !== null ? input.expected : readOptimisticLockExpected(input.request ?? null);
58
- assertOptimisticLock({
59
- resourceKind: input.resourceKind,
60
- resourceId: input.resourceId,
61
- expected,
62
- current: input.current,
63
- envValue: input.envValue
64
- });
65
- }
66
- function enforceRecordGoneIsConflict(input) {
67
- const config = resolveConfig(input.envValue);
68
- if (!isResourceLockEnabled(config, input.resourceKind)) return;
69
- const clientSupplied = input.expected !== void 0 && input.expected !== null ? input.expected : readOptimisticLockExpected(input.request ?? null);
70
- const expectedIso = toIsoOrNull(clientSupplied);
71
- if (expectedIso == null) return;
72
- throw new CrudHttpError(409, buildOptimisticLockConflictBody(expectedIso, expectedIso));
73
- }
74
- function createCommandOptimisticLockGuardService(options = {}) {
75
- const resolveExpected = options.resolveExpected ?? null;
76
- return {
77
- async enforce(input) {
78
- const config = resolveConfig(input.envValue);
79
- if (!isResourceLockEnabled(config, input.resourceKind)) return;
80
- const clientSupplied = input.expected !== void 0 && input.expected !== null ? input.expected : readOptimisticLockExpected(input.request ?? null);
81
- const expectedFromHeader = toIsoOrNull(clientSupplied);
82
- let expected = expectedFromHeader;
83
- if (resolveExpected) {
84
- const resolverInput = {
85
- expectedFromHeader,
86
- resourceKind: input.resourceKind,
87
- resourceId: input.resourceId
88
- };
89
- expected = await resolveExpected(resolverInput);
90
- }
91
- assertOptimisticLock({
92
- resourceKind: input.resourceKind,
93
- resourceId: input.resourceId,
94
- expected,
95
- current: input.current,
96
- envValue: input.envValue
97
- });
98
- }
99
- };
100
- }
101
- export {
102
- assertOptimisticLock,
103
- buildOptimisticLockConflictBody,
104
- createCommandOptimisticLockGuardService,
105
- enforceCommandOptimisticLock,
106
- enforceRecordGoneIsConflict,
107
- readOptimisticLockExpected
108
- };
109
- //# sourceMappingURL=optimistic-lock-command.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/crud/optimistic-lock-command.ts"],
4
- "sourcesContent": ["/**\n * Generalist command-level OSS optimistic-locking helper.\n *\n * The CRUD guard (`optimistic-lock.ts` + `makeCrudRoute`) only protects\n * mutations that flow through the CRUD factory. Domain writes implemented via\n * the Command pattern \u2014 sales document sub-resources (lines, adjustments,\n * shipments, payments, returns), status transitions, quote\u2192order conversion,\n * etc. \u2014 run their own logic inside a command handler and never reach the CRUD\n * guard for the **aggregate** they mutate. This helper lets any command enforce\n * the same `updated_at` version check against an arbitrary target record\n * (typically the aggregate root, e.g. the parent order/quote) and fail with the\n * identical structured 409 the CRUD path returns.\n *\n * Contract (mirrors the CRUD guard so clients see one behavior):\n * - The client sends the expected version via the\n * `x-om-ext-optimistic-lock-expected-updated-at` header (or a command\n * accepts it as a typed input field and passes it as `expected`).\n * - The command loads the current record (it usually already does) and passes\n * its `updated_at` as `current`.\n * - On mismatch the helper throws `CrudHttpError(409, OptimisticLockConflictBody)`.\n *\n * Strictly additive: when no expected token is present (no header, no input\n * field) the helper is a no-op, so existing API consumers that don't send the\n * header keep working. Respects the same `OM_OPTIMISTIC_LOCK` env contract\n * (default ON; `off` disables; allow-list scopes by `resourceKind`).\n *\n * Spec: .ai/specs/2026-05-25-oss-optimistic-locking.md (\u00A7 command-level checks)\n * .ai/specs/2026-05-28-optimistic-locking-coverage-completion.md (Phase 4)\n */\nimport { CrudHttpError } from './errors'\nimport {\n OPTIMISTIC_LOCK_CONFLICT_CODE,\n OPTIMISTIC_LOCK_CONFLICT_ERROR,\n OPTIMISTIC_LOCK_ENV_VAR,\n OPTIMISTIC_LOCK_HEADER_NAME,\n type OptimisticLockConflictBody,\n} from './optimistic-lock-headers'\nimport {\n normalizeIsoToken,\n parseOptimisticLockEnv,\n type OptimisticLockConfig,\n type OptimisticLockResolverInput,\n type ResolveExpectedUpdatedAt,\n} from './optimistic-lock'\n\nfunction toIsoOrNull(value: string | Date | null | undefined): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isFinite(ms) ? new Date(ms).toISOString() : null\n }\n const trimmed = String(value).trim()\n if (!trimmed) return null\n return normalizeIsoToken(trimmed)\n}\n\nfunction resolveConfig(envValue: string | null | undefined): OptimisticLockConfig {\n return parseOptimisticLockEnv(envValue !== undefined ? envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR])\n}\n\nfunction isResourceLockEnabled(config: OptimisticLockConfig, resourceKind: string): boolean {\n if (config.mode === 'off') return false\n if (config.mode === 'all') return true\n return config.entities.has(resourceKind.toLowerCase())\n}\n\n/**\n * Extract the expected `updated_at` token from a request's headers (or a bare\n * `Headers` object). Returns the trimmed header value, or `null` when absent /\n * empty. Does NOT normalize \u2014 `assertOptimisticLock` normalizes both sides.\n */\nexport function readOptimisticLockExpected(\n source: Request | Headers | null | undefined,\n): string | null {\n if (!source) return null\n const headers = source instanceof Headers\n ? source\n : (source as Request).headers instanceof Headers\n ? (source as Request).headers\n : null\n if (!headers) return null\n const direct = headers.get(OPTIMISTIC_LOCK_HEADER_NAME)\n if (typeof direct === 'string' && direct.trim().length > 0) return direct.trim()\n return null\n}\n\nexport function buildOptimisticLockConflictBody(\n currentIso: string,\n expectedIso: string,\n): OptimisticLockConflictBody {\n return {\n error: OPTIMISTIC_LOCK_CONFLICT_ERROR,\n code: OPTIMISTIC_LOCK_CONFLICT_CODE,\n currentUpdatedAt: currentIso,\n expectedUpdatedAt: expectedIso,\n }\n}\n\nexport type AssertOptimisticLockInput = {\n resourceKind: string\n resourceId: string\n /** Client-provided expected version (header value or typed input field). */\n expected: string | Date | null | undefined\n /** Current version loaded from the DB (typically the aggregate root's `updatedAt`). */\n current: string | Date | null | undefined\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Pure version assertion. Throws `CrudHttpError(409, OptimisticLockConflictBody)`\n * when the expected and current versions disagree.\n *\n * No-op (returns silently) when:\n * - the env disables the guard for this `resourceKind`,\n * - `expected` is missing / unparseable (strictly additive \u2014 clients that\n * don't send the token are never blocked),\n * - `current` is missing / unparseable (let the command's own 404 fire).\n */\nexport function assertOptimisticLock(input: AssertOptimisticLockInput): void {\n const config = resolveConfig(input.envValue)\n if (!isResourceLockEnabled(config, input.resourceKind)) return\n\n const expectedIso = toIsoOrNull(input.expected)\n if (expectedIso == null) return\n\n const currentIso = toIsoOrNull(input.current)\n if (currentIso == null) return\n\n if (currentIso === expectedIso) return\n\n throw new CrudHttpError(409, buildOptimisticLockConflictBody(currentIso, expectedIso))\n}\n\nexport type EnforceCommandOptimisticLockInput = {\n resourceKind: string\n resourceId: string\n /** Current version loaded from the DB (the aggregate root's `updatedAt`). */\n current: string | Date | null | undefined\n /**\n * Explicit expected version \u2014 wins over the request header. Use when the\n * command accepts the token as a typed input field instead of (or in addition\n * to) the extension header.\n */\n expected?: string | Date | null\n /** Request whose headers carry the expected token (e.g. `ctx.request`). */\n request?: Request | Headers | null\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Command-handler convenience: resolves the expected version from an explicit\n * override or the request header, then delegates to `assertOptimisticLock`.\n *\n * ```ts\n * enforceCommandOptimisticLock({\n * resourceKind: 'sales.order',\n * resourceId: order.id,\n * current: order.updatedAt,\n * request: ctx.request,\n * })\n * ```\n */\nexport function enforceCommandOptimisticLock(input: EnforceCommandOptimisticLockInput): void {\n const expected = input.expected !== undefined && input.expected !== null\n ? input.expected\n : readOptimisticLockExpected(input.request ?? null)\n assertOptimisticLock({\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n expected,\n current: input.current,\n envValue: input.envValue,\n })\n}\n\nexport type EnforceRecordGoneIsConflictInput = {\n resourceKind: string\n resourceId: string\n /** Explicit expected version \u2014 wins over the request header. */\n expected?: string | Date | null\n /** Request whose headers carry the expected token (e.g. `ctx.request`). */\n request?: Request | Headers | null\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Command-handler convenience for the *concurrent-delete* race: when a command\n * cannot find its target record (it was deleted in another tab) AND the client\n * opted into optimistic locking (sent the expected-version header / token),\n * throw the SAME structured `CrudHttpError(409, OptimisticLockConflictBody)`\n * the version-mismatch path returns \u2014 so a stale modal save surfaces the unified\n * \"Record changed\" conflict bar instead of a bare, generic 404.\n *\n * Strictly additive and fail-open: a no-op (returns silently) when the env\n * disables the guard for `resourceKind`, or when no expected token is present\n * (plain API consumers that never sent the header keep their existing 404).\n * The caller MUST still throw its own 404 afterwards for that no-token path:\n *\n * ```ts\n * if (!interaction) {\n * enforceRecordGoneIsConflict({ resourceKind: 'customers.interaction', resourceId: id, request: ctx.request })\n * throw new CrudHttpError(404, { error: 'Interaction not found' })\n * }\n * ```\n *\n * The gone record has no current version, so `currentUpdatedAt` echoes the\n * expected token (the body is used only for the conflict-bar copy + diagnostics;\n * the client keys off `code`, not the timestamps).\n */\nexport function enforceRecordGoneIsConflict(input: EnforceRecordGoneIsConflictInput): void {\n const config = resolveConfig(input.envValue)\n if (!isResourceLockEnabled(config, input.resourceKind)) return\n const clientSupplied = input.expected !== undefined && input.expected !== null\n ? input.expected\n : readOptimisticLockExpected(input.request ?? null)\n const expectedIso = toIsoOrNull(clientSupplied)\n if (expectedIso == null) return\n throw new CrudHttpError(409, buildOptimisticLockConflictBody(expectedIso, expectedIso))\n}\n\n/**\n * DI-resolvable command-level optimistic-lock guard. This is the framework\n * seam that lets BOTH layers protect Command-pattern writes through one\n * contract:\n *\n * - **OSS** registers the default service (header/explicit token compare \u2014\n * identical to calling `enforceCommandOptimisticLock` directly).\n * - **Enterprise** (`record_locks`) re-registers the same DI key with a\n * `resolveExpected` that reads the held pessimistic lock's version, so a\n * stale command write fails with the same structured 409 WITHOUT any\n * command handler changing. Mirrors how enterprise already replaces the\n * CRUD-path `crudMutationGuardService` (see `optimistic-lock.ts`).\n *\n * Command handlers depend only on this interface (resolved from the container),\n * never on a concrete implementation \u2014 that is what makes the next-PR\n * enterprise extension a pure DI swap.\n */\nexport type CommandOptimisticLockGuardService = {\n /**\n * Enforce the version check for a command-level mutation against an\n * aggregate/record. Async because an enterprise resolver may load the\n * expected token from a lock record. No-op (resolves silently) when the env\n * disables the guard for `resourceKind` or when no expected token is\n * resolved \u2014 strictly additive, exactly like {@link enforceCommandOptimisticLock}.\n * Throws `CrudHttpError(409, OptimisticLockConflictBody)` on mismatch.\n */\n enforce: (input: EnforceCommandOptimisticLockInput) => Promise<void>\n}\n\nexport type CreateCommandOptimisticLockGuardServiceOptions = {\n /**\n * Override how the expected version is derived. Receives\n * `{ expectedFromHeader, resourceKind, resourceId }` (where\n * `expectedFromHeader` is the normalized client-supplied token \u2014 explicit\n * input or request header) and returns the expected token (or `null` to\n * skip). Defaults to \"use the client-supplied token\", which is the OSS\n * behavior. The enterprise `record_locks` module plugs a lock-backed\n * resolver here. Mirrors the CRUD guard's `resolveExpected`.\n */\n resolveExpected?: ResolveExpectedUpdatedAt\n}\n\n/**\n * Build a {@link CommandOptimisticLockGuardService}. With no options it is\n * behaviourally identical to {@link enforceCommandOptimisticLock} (header/\n * explicit compare), so the OSS default is a thin wrapper. Pass\n * `resolveExpected` to override token resolution (enterprise extension point).\n */\nexport function createCommandOptimisticLockGuardService(\n options: CreateCommandOptimisticLockGuardServiceOptions = {},\n): CommandOptimisticLockGuardService {\n const resolveExpected: ResolveExpectedUpdatedAt | null = options.resolveExpected ?? null\n return {\n async enforce(input: EnforceCommandOptimisticLockInput): Promise<void> {\n const config = resolveConfig(input.envValue)\n if (!isResourceLockEnabled(config, input.resourceKind)) return\n\n const clientSupplied = input.expected !== undefined && input.expected !== null\n ? input.expected\n : readOptimisticLockExpected(input.request ?? null)\n const expectedFromHeader = toIsoOrNull(clientSupplied)\n\n let expected: string | null = expectedFromHeader\n if (resolveExpected) {\n const resolverInput: OptimisticLockResolverInput = {\n expectedFromHeader,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n }\n expected = await resolveExpected(resolverInput)\n }\n\n assertOptimisticLock({\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n expected,\n current: input.current,\n envValue: input.envValue,\n })\n },\n }\n}\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
@@ -1,15 +0,0 @@
1
- const OPTIMISTIC_LOCK_MODULE_ID = "optimistic_lock";
2
- const OPTIMISTIC_LOCK_HEADER_NAME = "x-om-ext-optimistic-lock-expected-updated-at";
3
- const OPTIMISTIC_LOCK_CONFLICT_CODE = "optimistic_lock_conflict";
4
- const OPTIMISTIC_LOCK_CONFLICT_ERROR = "record_modified";
5
- const OPTIMISTIC_LOCK_ENV_VAR = "OM_OPTIMISTIC_LOCK";
6
- const OPTIMISTIC_LOCK_DEFAULT_PRIORITY = 50;
7
- export {
8
- OPTIMISTIC_LOCK_CONFLICT_CODE,
9
- OPTIMISTIC_LOCK_CONFLICT_ERROR,
10
- OPTIMISTIC_LOCK_DEFAULT_PRIORITY,
11
- OPTIMISTIC_LOCK_ENV_VAR,
12
- OPTIMISTIC_LOCK_HEADER_NAME,
13
- OPTIMISTIC_LOCK_MODULE_ID
14
- };
15
- //# sourceMappingURL=optimistic-lock-headers.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/crud/optimistic-lock-headers.ts"],
4
- "sourcesContent": ["/**\n * Wire constants for the OSS opt-in optimistic-locking guard.\n *\n * The header name follows the project's extension-header convention\n * (`x-om-ext-<moduleId>-<key>`, see `umes/extension-headers.ts`). The\n * module id used by env opt-in is `optimistic_lock` (snake_case), but\n * the HTTP header itself uses dash-separated `optimistic-lock` because\n * many HTTP intermediaries (nginx, some fetch implementations) strip\n * underscored header names \u2014 see RFC 7230 \u00A73.2.6.\n *\n * Spec: .ai/specs/2026-05-25-oss-optimistic-locking.md \u00A73.2\n */\nexport const OPTIMISTIC_LOCK_MODULE_ID = 'optimistic_lock'\n\nexport const OPTIMISTIC_LOCK_HEADER_NAME = 'x-om-ext-optimistic-lock-expected-updated-at'\n\nexport const OPTIMISTIC_LOCK_CONFLICT_CODE = 'optimistic_lock_conflict'\n\nexport const OPTIMISTIC_LOCK_CONFLICT_ERROR = 'record_modified'\n\nexport const OPTIMISTIC_LOCK_ENV_VAR = 'OM_OPTIMISTIC_LOCK'\n\nexport const OPTIMISTIC_LOCK_DEFAULT_PRIORITY = 50\n\nexport type OptimisticLockConflictBody = {\n error: typeof OPTIMISTIC_LOCK_CONFLICT_ERROR\n code: typeof OPTIMISTIC_LOCK_CONFLICT_CODE\n currentUpdatedAt: string\n expectedUpdatedAt: string\n}\n"],
5
- "mappings": "AAYO,MAAM,4BAA4B;AAElC,MAAM,8BAA8B;AAEpC,MAAM,gCAAgC;AAEtC,MAAM,iCAAiC;AAEvC,MAAM,0BAA0B;AAEhC,MAAM,mCAAmC;",
6
- "names": []
7
- }
@@ -1,52 +0,0 @@
1
- const GLOBAL_KEY = "__openMercatoOptimisticLockReaders__";
2
- function readGlobal() {
3
- try {
4
- const value = globalThis[GLOBAL_KEY];
5
- if (value && typeof value === "object") {
6
- return value;
7
- }
8
- return {};
9
- } catch {
10
- return {};
11
- }
12
- }
13
- function writeGlobal(value) {
14
- try {
15
- ;
16
- globalThis[GLOBAL_KEY] = value;
17
- } catch {
18
- }
19
- }
20
- function registerOptimisticLockReaders(readers) {
21
- const existing = readGlobal();
22
- writeGlobal({ ...existing, ...readers });
23
- }
24
- function registerOptimisticLockReaderIfAbsent(readers) {
25
- const existing = readGlobal();
26
- const next = { ...existing };
27
- const written = [];
28
- for (const [key, reader] of Object.entries(readers)) {
29
- if (!(key in existing)) {
30
- next[key] = reader;
31
- written.push(key);
32
- }
33
- }
34
- if (written.length > 0) writeGlobal(next);
35
- return written;
36
- }
37
- function getAllOptimisticLockReaders() {
38
- return readGlobal();
39
- }
40
- function clearOptimisticLockReadersForTests() {
41
- try {
42
- delete globalThis[GLOBAL_KEY];
43
- } catch {
44
- }
45
- }
46
- export {
47
- clearOptimisticLockReadersForTests,
48
- getAllOptimisticLockReaders,
49
- registerOptimisticLockReaderIfAbsent,
50
- registerOptimisticLockReaders
51
- };
52
- //# sourceMappingURL=optimistic-lock-store.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/crud/optimistic-lock-store.ts"],
4
- "sourcesContent": ["/**\n * Global registry of OSS optimistic-lock readers keyed by `resourceKind`.\n *\n * Multiple modules can contribute readers without conflicting on the\n * single Awilix `crudMutationGuardService` slot: each module calls\n * `registerOptimisticLockReaders({...})` from its `di.ts` at module-load\n * time, and any one of them can register the\n * `crudMutationGuardService` Awilix binding (Awilix replaces same-key\n * registrations, so the *last loaded* wins \u2014 but every binding points\n * to the same store-backed factory, so the resulting guard set is\n * identical regardless of order).\n *\n * Mirrors the `mutation-guard-store.ts` HMR-safe globalThis pattern.\n *\n * Spec: .ai/specs/2026-05-25-oss-optimistic-locking.md\n */\nimport type { OptimisticLockCurrentReader } from './optimistic-lock'\n\nconst GLOBAL_KEY = '__openMercatoOptimisticLockReaders__'\n\nfunction readGlobal(): Record<string, OptimisticLockCurrentReader> {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_KEY]\n if (value && typeof value === 'object') {\n return value as Record<string, OptimisticLockCurrentReader>\n }\n return {}\n } catch {\n return {}\n }\n}\n\nfunction writeGlobal(value: Record<string, OptimisticLockCurrentReader>): void {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_KEY] = value\n } catch {\n // ignore global assignment failures in restricted runtimes\n }\n}\n\n/**\n * Register optimistic-lock readers for one or more `resourceKind` values.\n * Idempotent for same-key calls (later registration overrides earlier).\n */\nexport function registerOptimisticLockReaders(\n readers: Record<string, OptimisticLockCurrentReader>,\n): void {\n const existing = readGlobal()\n writeGlobal({ ...existing, ...readers })\n}\n\n/**\n * Register optimistic-lock readers only for keys that have no reader yet.\n * Use this for fallback / generic registrations (e.g. the auto-registration\n * driven by `makeCrudRoute`) so module-level hand-wired readers \u2014 which\n * register first via `di.ts` \u2014 always win.\n *\n * Returns the set of keys that were actually written, which makes the helper\n * easy to assert on in tests and useful for diagnostics in callers.\n */\nexport function registerOptimisticLockReaderIfAbsent(\n readers: Record<string, OptimisticLockCurrentReader>,\n): string[] {\n const existing = readGlobal()\n const next: Record<string, OptimisticLockCurrentReader> = { ...existing }\n const written: string[] = []\n for (const [key, reader] of Object.entries(readers)) {\n if (!(key in existing)) {\n next[key] = reader\n written.push(key)\n }\n }\n if (written.length > 0) writeGlobal(next)\n return written\n}\n\nexport function getAllOptimisticLockReaders(): Record<string, OptimisticLockCurrentReader> {\n return readGlobal()\n}\n\nexport function clearOptimisticLockReadersForTests(): void {\n try {\n delete (globalThis as Record<string, unknown>)[GLOBAL_KEY]\n } catch {\n // ignore\n }\n}\n"],
5
- "mappings": "AAkBA,MAAM,aAAa;AAEnB,SAAS,aAA0D;AACjE,MAAI;AACF,UAAM,QAAS,WAAuC,UAAU;AAChE,QAAI,SAAS,OAAO,UAAU,UAAU;AACtC,aAAO;AAAA,IACT;AACA,WAAO,CAAC;AAAA,EACV,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,YAAY,OAA0D;AAC7E,MAAI;AACF;AAAC,IAAC,WAAuC,UAAU,IAAI;AAAA,EACzD,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,8BACd,SACM;AACN,QAAM,WAAW,WAAW;AAC5B,cAAY,EAAE,GAAG,UAAU,GAAG,QAAQ,CAAC;AACzC;AAWO,SAAS,qCACd,SACU;AACV,QAAM,WAAW,WAAW;AAC5B,QAAM,OAAoD,EAAE,GAAG,SAAS;AACxE,QAAM,UAAoB,CAAC;AAC3B,aAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,QAAI,EAAE,OAAO,WAAW;AACtB,WAAK,GAAG,IAAI;AACZ,cAAQ,KAAK,GAAG;AAAA,IAClB;AAAA,EACF;AACA,MAAI,QAAQ,SAAS,EAAG,aAAY,IAAI;AACxC,SAAO;AACT;AAEO,SAAS,8BAA2E;AACzF,SAAO,WAAW;AACpB;AAEO,SAAS,qCAA2C;AACzD,MAAI;AACF,WAAQ,WAAuC,UAAU;AAAA,EAC3D,QAAQ;AAAA,EAER;AACF;",
6
- "names": []
7
- }