@open-mercato/enterprise 0.6.6-develop.6317.1.b3be10ab84 → 0.6.6-develop.6331.1.a33b8e99b7

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:enterprise] found 295 entry points
1
+ [build:enterprise] found 325 entry points
2
2
  [build:enterprise] built successfully
@@ -1,4 +1,7 @@
1
1
  import { asFunction } from "awilix";
2
+ import { createOptimisticLockGuardService } from "@open-mercato/shared/lib/crud/optimistic-lock";
3
+ import { getAllOptimisticLockReaders } from "@open-mercato/shared/lib/crud/optimistic-lock-store";
4
+ import { createCommandOptimisticLockGuardService } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
2
5
  import { createRecordLockService } from "./lib/recordLockService.js";
3
6
  import { createRecordLockCrudMutationGuardService } from "./lib/crudMutationGuardService.js";
4
7
  function register(container) {
@@ -11,8 +14,27 @@ function register(container) {
11
14
  rbacService: rbacService ?? null
12
15
  })
13
16
  ).scoped(),
17
+ // CRUD guard decorator: chains the OSS `updated_at` floor first (built here
18
+ // because this DI key overrides the platform default), then adds the
19
+ // record_locks enrichment. record_locks can only ADD a 409, never skip the
20
+ // floor (S1/H2). Spec: .ai/specs/enterprise/2026-06-09-record-locks-unified-coverage.md (Phase 0)
14
21
  crudMutationGuardService: asFunction(
15
- (recordLockService) => createRecordLockCrudMutationGuardService(recordLockService)
22
+ (recordLockService, em) => createRecordLockCrudMutationGuardService(
23
+ recordLockService,
24
+ createOptimisticLockGuardService({
25
+ getEm: () => em,
26
+ readers: getAllOptimisticLockReaders()
27
+ })
28
+ )
29
+ ).scoped(),
30
+ // Command guard override: lock-backed `resolveExpected` derived from
31
+ // authoritative server state (never requiring a client lock token, H2),
32
+ // awaited by `enforceCommandOptimisticLockWithGuards`. The OSS floor still
33
+ // runs first inside that runner.
34
+ commandOptimisticLockGuardService: asFunction(
35
+ (recordLockService) => createCommandOptimisticLockGuardService({
36
+ resolveExpected: ({ expectedFromHeader, resourceKind }) => recordLockService.resolveExpectedVersion({ expectedFromHeader, resourceKind })
37
+ })
16
38
  ).scoped()
17
39
  });
18
40
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/record_locks/di.ts"],
4
- "sourcesContent": ["import { asFunction } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { createRecordLockService } from './lib/recordLockService'\nimport type { RecordLockService } from './lib/recordLockService'\nimport { createRecordLockCrudMutationGuardService } from './lib/crudMutationGuardService'\n\nexport function register(container: AppContainer) {\n container.register({\n recordLockService: asFunction((\n em: EntityManager,\n moduleConfigService?: ModuleConfigService | null,\n actionLogService?: ActionLogService | null,\n rbacService?: RbacService | null,\n ) =>\n createRecordLockService({\n em,\n moduleConfigService: moduleConfigService ?? null,\n actionLogService: actionLogService ?? null,\n rbacService: rbacService ?? null,\n }),\n ).scoped(),\n crudMutationGuardService: asFunction((recordLockService: RecordLockService) =>\n createRecordLockCrudMutationGuardService(recordLockService),\n ).scoped(),\n })\n}\n"],
5
- "mappings": "AAAA,SAAS,kBAAkB;AAM3B,SAAS,+BAA+B;AAExC,SAAS,gDAAgD;AAElD,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,mBAAmB;AAAA,MAAW,CAC5B,IACA,qBACA,kBACA,gBAEA,wBAAwB;AAAA,QACtB;AAAA,QACA,qBAAqB,uBAAuB;AAAA,QAC5C,kBAAkB,oBAAoB;AAAA,QACtC,aAAa,eAAe;AAAA,MAC9B,CAAC;AAAA,IACH,EAAE,OAAO;AAAA,IACT,0BAA0B;AAAA,MAAW,CAAC,sBACpC,yCAAyC,iBAAiB;AAAA,IAC5D,EAAE,OAAO;AAAA,EACX,CAAC;AACH;",
4
+ "sourcesContent": ["import { asFunction } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\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 { createRecordLockService } from './lib/recordLockService'\nimport type { RecordLockService } from './lib/recordLockService'\nimport { createRecordLockCrudMutationGuardService } from './lib/crudMutationGuardService'\n\nexport function register(container: AppContainer) {\n container.register({\n recordLockService: asFunction((\n em: EntityManager,\n moduleConfigService?: ModuleConfigService | null,\n actionLogService?: ActionLogService | null,\n rbacService?: RbacService | null,\n ) =>\n createRecordLockService({\n em,\n moduleConfigService: moduleConfigService ?? null,\n actionLogService: actionLogService ?? null,\n rbacService: rbacService ?? null,\n }),\n ).scoped(),\n // CRUD guard decorator: chains the OSS `updated_at` floor first (built here\n // because this DI key overrides the platform default), then adds the\n // record_locks enrichment. record_locks can only ADD a 409, never skip the\n // floor (S1/H2). Spec: .ai/specs/enterprise/2026-06-09-record-locks-unified-coverage.md (Phase 0)\n crudMutationGuardService: asFunction((\n recordLockService: RecordLockService,\n em: EntityManager,\n ) =>\n createRecordLockCrudMutationGuardService(\n recordLockService,\n createOptimisticLockGuardService({\n getEm: () => em,\n readers: getAllOptimisticLockReaders(),\n }),\n ),\n ).scoped(),\n // Command guard override: lock-backed `resolveExpected` derived from\n // authoritative server state (never requiring a client lock token, H2),\n // awaited by `enforceCommandOptimisticLockWithGuards`. The OSS floor still\n // runs first inside that runner.\n commandOptimisticLockGuardService: asFunction((recordLockService: RecordLockService) =>\n createCommandOptimisticLockGuardService({\n resolveExpected: ({ expectedFromHeader, resourceKind }) =>\n recordLockService.resolveExpectedVersion({ expectedFromHeader, resourceKind }),\n }),\n ).scoped(),\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAM3B,SAAS,wCAAwC;AACjD,SAAS,mCAAmC;AAC5C,SAAS,+CAA+C;AACxD,SAAS,+BAA+B;AAExC,SAAS,gDAAgD;AAElD,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,mBAAmB;AAAA,MAAW,CAC5B,IACA,qBACA,kBACA,gBAEA,wBAAwB;AAAA,QACtB;AAAA,QACA,qBAAqB,uBAAuB;AAAA,QAC5C,kBAAkB,oBAAoB;AAAA,QACtC,aAAa,eAAe;AAAA,MAC9B,CAAC;AAAA,IACH,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKT,0BAA0B;AAAA,MAAW,CACnC,mBACA,OAEA;AAAA,QACE;AAAA,QACA,iCAAiC;AAAA,UAC/B,OAAO,MAAM;AAAA,UACb,SAAS,4BAA4B;AAAA,QACvC,CAAC;AAAA,MACH;AAAA,IACF,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKT,mCAAmC;AAAA,MAAW,CAAC,sBAC7C,wCAAwC;AAAA,QACtC,iBAAiB,CAAC,EAAE,oBAAoB,aAAa,MACnD,kBAAkB,uBAAuB,EAAE,oBAAoB,aAAa,CAAC;AAAA,MACjF,CAAC;AAAA,IACH,EAAE,OAAO;AAAA,EACX,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -1,36 +1,66 @@
1
+ import {
2
+ parseOptimisticLockEnv
3
+ } from "@open-mercato/shared/lib/crud/optimistic-lock";
4
+ import { OPTIMISTIC_LOCK_ENV_VAR } from "@open-mercato/shared/lib/crud/optimistic-lock-headers";
1
5
  import { readRecordLockHeaders } from "./recordLockService.js";
6
+ import { isRecordLockingEnabledForResource } from "./config.js";
2
7
  function resolveRecordLockMutationMethod(operation) {
3
8
  if (operation === "delete") return "DELETE";
4
9
  return "PUT";
5
10
  }
6
- function createRecordLockCrudMutationGuardService(recordLockService) {
11
+ function resolveConfig(envValue) {
12
+ return parseOptimisticLockEnv(envValue !== void 0 ? envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR]);
13
+ }
14
+ function createRecordLockCrudMutationGuardService(recordLockService, ossFloorGuardService, options = {}) {
15
+ async function isRecordLockEnrichmentEnabled(resourceKind) {
16
+ try {
17
+ const settings = await recordLockService.getSettings();
18
+ return isRecordLockingEnabledForResource(settings, resourceKind);
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
7
23
  return {
8
24
  async validateMutation(input) {
9
- const result = await recordLockService.validateMutation({
10
- tenantId: input.tenantId,
11
- organizationId: input.organizationId ?? null,
12
- userId: input.userId,
13
- resourceKind: input.resourceKind,
14
- resourceId: input.resourceId,
15
- method: resolveRecordLockMutationMethod(input.operation),
16
- headers: readRecordLockHeaders(input.requestHeaders),
17
- mutationPayload: input.mutationPayload ?? null
18
- });
19
- if (result.ok) {
25
+ const config = resolveConfig(options.envValue);
26
+ if (config.mode === "off") {
27
+ return { ok: true, shouldRunAfterSuccess: false, metadata: null };
28
+ }
29
+ const floorResult = await ossFloorGuardService.validateMutation(input);
30
+ if (!floorResult.ok) return floorResult;
31
+ if (!await isRecordLockEnrichmentEnabled(input.resourceKind)) {
32
+ return { ok: true, shouldRunAfterSuccess: false, metadata: null };
33
+ }
34
+ let enrichmentResult;
35
+ try {
36
+ enrichmentResult = await recordLockService.validateMutation({
37
+ tenantId: input.tenantId,
38
+ organizationId: input.organizationId ?? null,
39
+ userId: input.userId,
40
+ resourceKind: input.resourceKind,
41
+ resourceId: input.resourceId,
42
+ method: resolveRecordLockMutationMethod(input.operation),
43
+ headers: readRecordLockHeaders(input.requestHeaders),
44
+ mutationPayload: input.mutationPayload ?? null
45
+ });
46
+ } catch {
47
+ return { ok: true, shouldRunAfterSuccess: false, metadata: null };
48
+ }
49
+ if (enrichmentResult.ok) {
20
50
  return {
21
51
  ok: true,
22
- shouldRunAfterSuccess: result.resourceEnabled,
52
+ shouldRunAfterSuccess: enrichmentResult.resourceEnabled,
23
53
  metadata: null
24
54
  };
25
55
  }
26
56
  return {
27
57
  ok: false,
28
- status: result.status,
58
+ status: enrichmentResult.status,
29
59
  body: {
30
- error: result.error,
31
- code: result.code,
32
- lock: result.lock ?? null,
33
- conflict: result.conflict ?? null
60
+ error: enrichmentResult.error,
61
+ code: enrichmentResult.code,
62
+ lock: enrichmentResult.lock ?? null,
63
+ conflict: enrichmentResult.conflict ?? null
34
64
  }
35
65
  };
36
66
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/record_locks/lib/crudMutationGuardService.ts"],
4
- "sourcesContent": ["import type {\n CrudMutationGuardAfterSuccessInput,\n CrudMutationGuardValidateInput,\n CrudMutationGuardValidationResult,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { readRecordLockHeaders, type RecordLockService } from './recordLockService'\n\nexport type RecordLockCrudMutationGuardService = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction resolveRecordLockMutationMethod(operation: CrudMutationGuardValidateInput['operation']): 'PUT' | 'DELETE' {\n if (operation === 'delete') return 'DELETE'\n return 'PUT'\n}\n\nexport function createRecordLockCrudMutationGuardService(\n recordLockService: RecordLockService,\n): RecordLockCrudMutationGuardService {\n return {\n async validateMutation(input) {\n const result = await recordLockService.validateMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n method: resolveRecordLockMutationMethod(input.operation),\n headers: readRecordLockHeaders(input.requestHeaders),\n mutationPayload: input.mutationPayload ?? null,\n })\n\n if (result.ok) {\n return {\n ok: true,\n shouldRunAfterSuccess: result.resourceEnabled,\n metadata: null,\n }\n }\n\n return {\n ok: false,\n status: result.status,\n body: {\n error: result.error,\n code: result.code,\n lock: result.lock ?? null,\n conflict: result.conflict ?? null,\n },\n }\n },\n\n async afterMutationSuccess(input) {\n const method = resolveRecordLockMutationMethod(input.operation)\n const headers = readRecordLockHeaders(input.requestHeaders)\n\n await recordLockService.releaseAfterMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: headers.token,\n reason: 'saved',\n })\n\n await Promise.allSettled([\n recordLockService.emitIncomingChangesNotificationAfterMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n method,\n }),\n recordLockService.emitRecordDeletedNotificationAfterMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n method,\n }),\n ])\n },\n }\n}\n"],
5
- "mappings": "AAKA,SAAS,6BAAqD;AAO9D,SAAS,gCAAgC,WAA0E;AACjH,MAAI,cAAc,SAAU,QAAO;AACnC,SAAO;AACT;AAEO,SAAS,yCACd,mBACoC;AACpC,SAAO;AAAA,IACL,MAAM,iBAAiB,OAAO;AAC5B,YAAM,SAAS,MAAM,kBAAkB,iBAAiB;AAAA,QACtD,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM,kBAAkB;AAAA,QACxC,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,QAAQ,gCAAgC,MAAM,SAAS;AAAA,QACvD,SAAS,sBAAsB,MAAM,cAAc;AAAA,QACnD,iBAAiB,MAAM,mBAAmB;AAAA,MAC5C,CAAC;AAED,UAAI,OAAO,IAAI;AACb,eAAO;AAAA,UACL,IAAI;AAAA,UACJ,uBAAuB,OAAO;AAAA,UAC9B,UAAU;AAAA,QACZ;AAAA,MACF;AAEA,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,OAAO;AAAA,QACf,MAAM;AAAA,UACJ,OAAO,OAAO;AAAA,UACd,MAAM,OAAO;AAAA,UACb,MAAM,OAAO,QAAQ;AAAA,UACrB,UAAU,OAAO,YAAY;AAAA,QAC/B;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,qBAAqB,OAAO;AAChC,YAAM,SAAS,gCAAgC,MAAM,SAAS;AAC9D,YAAM,UAAU,sBAAsB,MAAM,cAAc;AAE1D,YAAM,kBAAkB,qBAAqB;AAAA,QAC3C,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM,kBAAkB;AAAA,QACxC,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,OAAO,QAAQ;AAAA,QACf,QAAQ;AAAA,MACV,CAAC;AAED,YAAM,QAAQ,WAAW;AAAA,QACvB,kBAAkB,6CAA6C;AAAA,UAC7D,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM,kBAAkB;AAAA,UACxC,QAAQ,MAAM;AAAA,UACd,cAAc,MAAM;AAAA,UACpB,YAAY,MAAM;AAAA,UAClB;AAAA,QACF,CAAC;AAAA,QACD,kBAAkB,2CAA2C;AAAA,UAC3D,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM,kBAAkB;AAAA,UACxC,QAAQ,MAAM;AAAA,UACd,cAAc,MAAM;AAAA,UACpB,YAAY,MAAM;AAAA,UAClB;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type {\n CrudMutationGuardAfterSuccessInput,\n CrudMutationGuardValidateInput,\n CrudMutationGuardValidationResult,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport {\n parseOptimisticLockEnv,\n type OptimisticLockConfig,\n} from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { OPTIMISTIC_LOCK_ENV_VAR } from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\nimport { readRecordLockHeaders, type RecordLockService } from './recordLockService'\nimport { isRecordLockingEnabledForResource } from './config'\n\nexport type RecordLockCrudMutationGuardService = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\n/** The OSS floor service this decorator chains. Same shape as the platform default. */\nexport type OssCrudMutationGuardServiceLike = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n}\n\nfunction resolveRecordLockMutationMethod(operation: CrudMutationGuardValidateInput['operation']): 'PUT' | 'DELETE' {\n if (operation === 'delete') return 'DELETE'\n return 'PUT'\n}\n\nfunction resolveConfig(envValue: string | null | undefined): OptimisticLockConfig {\n return parseOptimisticLockEnv(envValue !== undefined ? envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR])\n}\n\nexport type CreateRecordLockCrudMutationGuardServiceOptions = {\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Enterprise CRUD mutation guard. A **decorator** over the OSS optimistic-lock\n * floor (S1/H2): it never replaces the floor, only adds blocks.\n *\n * Evaluation order (matches the spec's guard-layering chain):\n * 1. `OM_OPTIMISTIC_LOCK=off` \u2192 pure pass-through (single global kill switch;\n * neither floor nor enrichment runs).\n * 2. OSS `updated_at` floor runs first (delegated to the default OSS guard the\n * enterprise registration overrides). A stale write 409s here regardless of\n * any record-lock token / widget state \u2014 so a tokenless API/CLI client is\n * still caught (H1/H2).\n * 3. Only if the floor passes, AND record_locks is enabled for the resource in\n * settings, the enterprise `validateMutation` enrichment runs (pessimistic\n * lock + action-log diff). It can only ADD a 409, never relax the floor.\n *\n * Fail-closed (H3): if the enterprise enrichment throws, the decorator degrades\n * to the floor result (floor-pass \u21D2 allow), never to \"skip the guard\".\n */\nexport function createRecordLockCrudMutationGuardService(\n recordLockService: RecordLockService,\n ossFloorGuardService: OssCrudMutationGuardServiceLike,\n options: CreateRecordLockCrudMutationGuardServiceOptions = {},\n): RecordLockCrudMutationGuardService {\n async function isRecordLockEnrichmentEnabled(resourceKind: string): Promise<boolean> {\n try {\n const settings = await recordLockService.getSettings()\n return isRecordLockingEnabledForResource(settings, resourceKind)\n } catch {\n return false\n }\n }\n\n return {\n async validateMutation(input) {\n const config = resolveConfig(options.envValue)\n if (config.mode === 'off') {\n // Single global kill switch: neither floor nor enrichment runs.\n return { ok: true, shouldRunAfterSuccess: false, metadata: null }\n }\n\n // 1. OSS floor \u2014 always runs first, independent of any client lock token.\n const floorResult = await ossFloorGuardService.validateMutation(input)\n if (!floorResult.ok) return floorResult\n\n // 2. Enterprise enrichment \u2014 only when the resource is enabled in settings.\n if (!(await isRecordLockEnrichmentEnabled(input.resourceKind))) {\n return { ok: true, shouldRunAfterSuccess: false, metadata: null }\n }\n\n // 3. Fail-closed: a throwing enrichment degrades to the (passed) floor.\n let enrichmentResult: Awaited<ReturnType<RecordLockService['validateMutation']>>\n try {\n enrichmentResult = await recordLockService.validateMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n method: resolveRecordLockMutationMethod(input.operation),\n headers: readRecordLockHeaders(input.requestHeaders),\n mutationPayload: input.mutationPayload ?? null,\n })\n } catch {\n return { ok: true, shouldRunAfterSuccess: false, metadata: null }\n }\n\n if (enrichmentResult.ok) {\n return {\n ok: true,\n shouldRunAfterSuccess: enrichmentResult.resourceEnabled,\n metadata: null,\n }\n }\n\n return {\n ok: false,\n status: enrichmentResult.status,\n body: {\n error: enrichmentResult.error,\n code: enrichmentResult.code,\n lock: enrichmentResult.lock ?? null,\n conflict: enrichmentResult.conflict ?? null,\n },\n }\n },\n\n async afterMutationSuccess(input) {\n const method = resolveRecordLockMutationMethod(input.operation)\n const headers = readRecordLockHeaders(input.requestHeaders)\n\n await recordLockService.releaseAfterMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: headers.token,\n reason: 'saved',\n })\n\n await Promise.allSettled([\n recordLockService.emitIncomingChangesNotificationAfterMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n method,\n }),\n recordLockService.emitRecordDeletedNotificationAfterMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n method,\n }),\n ])\n },\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -262,6 +262,22 @@ class RecordLockService {
262
262
  await this.moduleConfigService.setValue(RECORD_LOCKS_MODULE_ID, RECORD_LOCKS_SETTINGS_NAME, settings);
263
263
  return settings;
264
264
  }
265
+ /**
266
+ * Command-guard `resolveExpected` seam (Phase 0 / S1). Derives the expected
267
+ * version token the OSS command floor compares against the record's current
268
+ * server-side `updated_at`.
269
+ *
270
+ * The compare is server-authoritative: the command handler already loaded the
271
+ * record from the DB and passes its `updated_at` as `current`, so a stale
272
+ * write is detected from the version header alone — NEVER requiring a client
273
+ * record-lock token (H2). When record_locks is not enabled for the resource
274
+ * (settings off), this returns the header token unchanged so the floor still
275
+ * runs (pure floor behavior). The richer pessimistic/action-log conflict
276
+ * detection runs through `validateMutation` at the CRUD layer.
277
+ */
278
+ async resolveExpectedVersion(input) {
279
+ return input.expectedFromHeader;
280
+ }
265
281
  async acquire(input) {
266
282
  this.scheduleCleanup(input.tenantId);
267
283
  const settings = await this.getSettings();