@open-mercato/core 0.6.6-develop.6304.1.4cf2b975cb → 0.6.6-develop.6309.1.983aeec27a

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.
@@ -12,6 +12,13 @@ function normalizeIsoToken(raw) {
12
12
  if (!Number.isFinite(ms)) return null;
13
13
  return new Date(ms).toISOString();
14
14
  }
15
+ const RECORD_LOCK_RESOLUTION_HEADER_NAME = "x-om-record-lock-resolution";
16
+ const RECORD_LOCK_OVERRIDE_RESOLUTIONS = /* @__PURE__ */ new Set(["accept_mine", "merged"]);
17
+ function isAuthorizedRecordLockOverride(headers) {
18
+ const raw = headers.get(RECORD_LOCK_RESOLUTION_HEADER_NAME);
19
+ if (typeof raw !== "string") return false;
20
+ return RECORD_LOCK_OVERRIDE_RESOLUTIONS.has(raw.trim().toLowerCase());
21
+ }
15
22
  const optimisticLockGuard = {
16
23
  id: "customers.optimistic-lock",
17
24
  targetEntity: "*",
@@ -20,6 +27,7 @@ const optimisticLockGuard = {
20
27
  async validate(input) {
21
28
  const config = parseOptimisticLockEnv(process.env[OPTIMISTIC_LOCK_ENV_VAR]);
22
29
  if (config.mode === "off") return { ok: true };
30
+ if (isAuthorizedRecordLockOverride(input.requestHeaders)) return { ok: true };
23
31
  const enabled = config.mode === "all" || config.entities.has(input.resourceKind.toLowerCase());
24
32
  if (!enabled) return { ok: true };
25
33
  const readers = getAllOptimisticLockReaders();
@@ -61,6 +69,7 @@ const optimisticLockGuard = {
61
69
  };
62
70
  const guards = [optimisticLockGuard];
63
71
  export {
64
- guards
72
+ guards,
73
+ isAuthorizedRecordLockOverride
65
74
  };
66
75
  //# sourceMappingURL=guards.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/data/guards.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { MutationGuard } from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { parseOptimisticLockEnv } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport {\n OPTIMISTIC_LOCK_CONFLICT_CODE,\n OPTIMISTIC_LOCK_CONFLICT_ERROR,\n OPTIMISTIC_LOCK_ENV_VAR,\n OPTIMISTIC_LOCK_HEADER_NAME,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\n\nfunction normalizeIsoToken(raw: string): string | null {\n const ms = Date.parse(raw)\n if (!Number.isFinite(ms)) return null\n return new Date(ms).toISOString()\n}\n\nconst optimisticLockGuard: MutationGuard = {\n id: 'customers.optimistic-lock',\n targetEntity: '*',\n operations: ['update', 'delete'],\n priority: 100,\n async validate(input) {\n const config = parseOptimisticLockEnv(process.env[OPTIMISTIC_LOCK_ENV_VAR])\n if (config.mode === 'off') return { ok: true }\n const enabled = config.mode === 'all' || config.entities.has(input.resourceKind.toLowerCase())\n if (!enabled) return { ok: true }\n const readers = getAllOptimisticLockReaders()\n const reader = readers[input.resourceKind]\n if (!reader) return { ok: true }\n const expectedRaw = input.requestHeaders.get(OPTIMISTIC_LOCK_HEADER_NAME)\n if (!expectedRaw || expectedRaw.trim().length === 0) return { ok: true }\n const expectedIso = normalizeIsoToken(expectedRaw.trim())\n if (!expectedIso) return { ok: true }\n if (!input.resourceId) return { ok: true }\n const container = await createRequestContainer()\n let em: EntityManager\n try {\n em = container.resolve('em') as EntityManager\n } catch {\n return { ok: true }\n }\n const currentRaw = await reader(em, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n })\n if (currentRaw == null) return { ok: true }\n const currentIso = normalizeIsoToken(currentRaw)\n if (currentIso == null) return { ok: true }\n if (currentIso === expectedIso) return { ok: true }\n return {\n ok: false,\n status: 409,\n body: {\n error: OPTIMISTIC_LOCK_CONFLICT_ERROR,\n code: OPTIMISTIC_LOCK_CONFLICT_CODE,\n currentUpdatedAt: currentIso,\n expectedUpdatedAt: expectedIso,\n },\n }\n },\n}\n\nexport const guards: MutationGuard[] = [optimisticLockGuard]\n"],
5
- "mappings": "AAEA,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AACvC,SAAS,mCAAmC;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,kBAAkB,KAA4B;AACrD,QAAM,KAAK,KAAK,MAAM,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO;AACjC,SAAO,IAAI,KAAK,EAAE,EAAE,YAAY;AAClC;AAEA,MAAM,sBAAqC;AAAA,EACzC,IAAI;AAAA,EACJ,cAAc;AAAA,EACd,YAAY,CAAC,UAAU,QAAQ;AAAA,EAC/B,UAAU;AAAA,EACV,MAAM,SAAS,OAAO;AACpB,UAAM,SAAS,uBAAuB,QAAQ,IAAI,uBAAuB,CAAC;AAC1E,QAAI,OAAO,SAAS,MAAO,QAAO,EAAE,IAAI,KAAK;AAC7C,UAAM,UAAU,OAAO,SAAS,SAAS,OAAO,SAAS,IAAI,MAAM,aAAa,YAAY,CAAC;AAC7F,QAAI,CAAC,QAAS,QAAO,EAAE,IAAI,KAAK;AAChC,UAAM,UAAU,4BAA4B;AAC5C,UAAM,SAAS,QAAQ,MAAM,YAAY;AACzC,QAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,KAAK;AAC/B,UAAM,cAAc,MAAM,eAAe,IAAI,2BAA2B;AACxE,QAAI,CAAC,eAAe,YAAY,KAAK,EAAE,WAAW,EAAG,QAAO,EAAE,IAAI,KAAK;AACvE,UAAM,cAAc,kBAAkB,YAAY,KAAK,CAAC;AACxD,QAAI,CAAC,YAAa,QAAO,EAAE,IAAI,KAAK;AACpC,QAAI,CAAC,MAAM,WAAY,QAAO,EAAE,IAAI,KAAK;AACzC,UAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAI;AACJ,QAAI;AACF,WAAK,UAAU,QAAQ,IAAI;AAAA,IAC7B,QAAQ;AACN,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AACA,UAAM,aAAa,MAAM,OAAO,IAAI;AAAA,MAClC,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM;AAAA,MAClB,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,IAC1C,CAAC;AACD,QAAI,cAAc,KAAM,QAAO,EAAE,IAAI,KAAK;AAC1C,UAAM,aAAa,kBAAkB,UAAU;AAC/C,QAAI,cAAc,KAAM,QAAO,EAAE,IAAI,KAAK;AAC1C,QAAI,eAAe,YAAa,QAAO,EAAE,IAAI,KAAK;AAClD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,kBAAkB;AAAA,QAClB,mBAAmB;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,SAA0B,CAAC,mBAAmB;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { MutationGuard } from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { parseOptimisticLockEnv } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport {\n OPTIMISTIC_LOCK_CONFLICT_CODE,\n OPTIMISTIC_LOCK_CONFLICT_ERROR,\n OPTIMISTIC_LOCK_ENV_VAR,\n OPTIMISTIC_LOCK_HEADER_NAME,\n} from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\n\nfunction normalizeIsoToken(raw: string): string | null {\n const ms = Date.parse(raw)\n if (!Number.isFinite(ms)) return null\n return new Date(ms).toISOString()\n}\n\n/**\n * HTTP header carrying the enterprise `record_locks` conflict resolution the\n * client chose in the merge dialog. This guard only reads the header value off\n * the request (no enterprise import) \u2014 it is the integration contract between\n * the record-lock layer and this universal optimistic-lock floor.\n */\nconst RECORD_LOCK_RESOLUTION_HEADER_NAME = 'x-om-record-lock-resolution'\n\n/**\n * Resolutions that express an explicit, privileged intent to OVERWRITE the\n * incoming concurrent write (\"Keep mine\" / merged). When the request asserts\n * one of these, the record-lock guard (which runs first, at priority 0) has\n * already authorized the override against `canOverrideIncoming`, so the\n * stale-`updated_at` floor MUST defer to it \u2014 otherwise it rejects exactly the\n * overwrite the user just authorized, looping the merge dialog on a fresh 409\n * (issue #3601). `accept_incoming` is intentionally NOT here: that path reloads\n * with a fresh `updated_at` and must still pass through the floor normally.\n */\nconst RECORD_LOCK_OVERRIDE_RESOLUTIONS: ReadonlySet<string> = new Set(['accept_mine', 'merged'])\n\nexport function isAuthorizedRecordLockOverride(headers: Headers): boolean {\n const raw = headers.get(RECORD_LOCK_RESOLUTION_HEADER_NAME)\n if (typeof raw !== 'string') return false\n return RECORD_LOCK_OVERRIDE_RESOLUTIONS.has(raw.trim().toLowerCase())\n}\n\nconst optimisticLockGuard: MutationGuard = {\n id: 'customers.optimistic-lock',\n targetEntity: '*',\n operations: ['update', 'delete'],\n priority: 100,\n async validate(input) {\n const config = parseOptimisticLockEnv(process.env[OPTIMISTIC_LOCK_ENV_VAR])\n if (config.mode === 'off') return { ok: true }\n // A privileged record-lock \"Keep mine\" override deliberately overwrites the\n // concurrent write; the floor must not block it on the now-stale timestamp.\n if (isAuthorizedRecordLockOverride(input.requestHeaders)) return { ok: true }\n const enabled = config.mode === 'all' || config.entities.has(input.resourceKind.toLowerCase())\n if (!enabled) return { ok: true }\n const readers = getAllOptimisticLockReaders()\n const reader = readers[input.resourceKind]\n if (!reader) return { ok: true }\n const expectedRaw = input.requestHeaders.get(OPTIMISTIC_LOCK_HEADER_NAME)\n if (!expectedRaw || expectedRaw.trim().length === 0) return { ok: true }\n const expectedIso = normalizeIsoToken(expectedRaw.trim())\n if (!expectedIso) return { ok: true }\n if (!input.resourceId) return { ok: true }\n const container = await createRequestContainer()\n let em: EntityManager\n try {\n em = container.resolve('em') as EntityManager\n } catch {\n return { ok: true }\n }\n const currentRaw = await reader(em, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n })\n if (currentRaw == null) return { ok: true }\n const currentIso = normalizeIsoToken(currentRaw)\n if (currentIso == null) return { ok: true }\n if (currentIso === expectedIso) return { ok: true }\n return {\n ok: false,\n status: 409,\n body: {\n error: OPTIMISTIC_LOCK_CONFLICT_ERROR,\n code: OPTIMISTIC_LOCK_CONFLICT_CODE,\n currentUpdatedAt: currentIso,\n expectedUpdatedAt: expectedIso,\n },\n }\n },\n}\n\nexport const guards: MutationGuard[] = [optimisticLockGuard]\n"],
5
+ "mappings": "AAEA,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AACvC,SAAS,mCAAmC;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,kBAAkB,KAA4B;AACrD,QAAM,KAAK,KAAK,MAAM,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO;AACjC,SAAO,IAAI,KAAK,EAAE,EAAE,YAAY;AAClC;AAQA,MAAM,qCAAqC;AAY3C,MAAM,mCAAwD,oBAAI,IAAI,CAAC,eAAe,QAAQ,CAAC;AAExF,SAAS,+BAA+B,SAA2B;AACxE,QAAM,MAAM,QAAQ,IAAI,kCAAkC;AAC1D,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,SAAO,iCAAiC,IAAI,IAAI,KAAK,EAAE,YAAY,CAAC;AACtE;AAEA,MAAM,sBAAqC;AAAA,EACzC,IAAI;AAAA,EACJ,cAAc;AAAA,EACd,YAAY,CAAC,UAAU,QAAQ;AAAA,EAC/B,UAAU;AAAA,EACV,MAAM,SAAS,OAAO;AACpB,UAAM,SAAS,uBAAuB,QAAQ,IAAI,uBAAuB,CAAC;AAC1E,QAAI,OAAO,SAAS,MAAO,QAAO,EAAE,IAAI,KAAK;AAG7C,QAAI,+BAA+B,MAAM,cAAc,EAAG,QAAO,EAAE,IAAI,KAAK;AAC5E,UAAM,UAAU,OAAO,SAAS,SAAS,OAAO,SAAS,IAAI,MAAM,aAAa,YAAY,CAAC;AAC7F,QAAI,CAAC,QAAS,QAAO,EAAE,IAAI,KAAK;AAChC,UAAM,UAAU,4BAA4B;AAC5C,UAAM,SAAS,QAAQ,MAAM,YAAY;AACzC,QAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,KAAK;AAC/B,UAAM,cAAc,MAAM,eAAe,IAAI,2BAA2B;AACxE,QAAI,CAAC,eAAe,YAAY,KAAK,EAAE,WAAW,EAAG,QAAO,EAAE,IAAI,KAAK;AACvE,UAAM,cAAc,kBAAkB,YAAY,KAAK,CAAC;AACxD,QAAI,CAAC,YAAa,QAAO,EAAE,IAAI,KAAK;AACpC,QAAI,CAAC,MAAM,WAAY,QAAO,EAAE,IAAI,KAAK;AACzC,UAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAI;AACJ,QAAI;AACF,WAAK,UAAU,QAAQ,IAAI;AAAA,IAC7B,QAAQ;AACN,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AACA,UAAM,aAAa,MAAM,OAAO,IAAI;AAAA,MAClC,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM;AAAA,MAClB,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,IAC1C,CAAC;AACD,QAAI,cAAc,KAAM,QAAO,EAAE,IAAI,KAAK;AAC1C,UAAM,aAAa,kBAAkB,UAAU;AAC/C,QAAI,cAAc,KAAM,QAAO,EAAE,IAAI,KAAK;AAC1C,QAAI,eAAe,YAAa,QAAO,EAAE,IAAI,KAAK;AAClD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,kBAAkB;AAAA,QAClB,mBAAmB;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,SAA0B,CAAC,mBAAmB;",
6
6
  "names": []
7
7
  }
@@ -71,7 +71,7 @@ const channelCrudIndexer = {
71
71
  entityType: E.sales.sales_channel
72
72
  };
73
73
  async function loadChannelSnapshot(em, id) {
74
- const channel = await em.findOne(SalesChannel, { id, deletedAt: null });
74
+ const channel = await findOneWithDecryption(em, SalesChannel, { id, deletedAt: null }, {});
75
75
  if (!channel) return null;
76
76
  const custom = await loadCustomFieldSnapshot(em, {
77
77
  entityId: E.sales.sales_channel,
@@ -504,7 +504,7 @@ const createChannelCommand = {
504
504
  const after = payload?.after;
505
505
  if (!after) return;
506
506
  const em = ctx.container.resolve("em").fork();
507
- const record = await em.findOne(SalesChannel, { id: after.id });
507
+ const record = await findOneWithDecryption(em, SalesChannel, { id: after.id }, {}, { tenantId: after.tenantId, organizationId: after.organizationId });
508
508
  if (!record) return;
509
509
  ensureTenantScope(ctx, record.tenantId);
510
510
  ensureOrganizationScope(ctx, record.organizationId);
@@ -578,7 +578,7 @@ const updateChannelCommand = {
578
578
  async execute(rawInput, ctx) {
579
579
  const { parsed, custom } = parseWithCustomFields(channelUpdateSchema, rawInput);
580
580
  const em = ctx.container.resolve("em").fork();
581
- const record = await em.findOne(SalesChannel, { id: parsed.id, deletedAt: null });
581
+ const record = await findOneWithDecryption(em, SalesChannel, { id: parsed.id, deletedAt: null }, {}, { tenantId: parsed.tenantId, organizationId: parsed.organizationId });
582
582
  if (!record) throw new CrudHttpError(404, { error: "Channel not found" });
583
583
  const dataEngine = ctx.container.resolve("dataEngine");
584
584
  const scope = resolveScopeFromUpdate(record, parsed, ctx);
@@ -670,7 +670,7 @@ const updateChannelCommand = {
670
670
  const before = payload?.before;
671
671
  if (!before) return;
672
672
  const em = ctx.container.resolve("em").fork();
673
- let record = await em.findOne(SalesChannel, { id: before.id });
673
+ let record = await findOneWithDecryption(em, SalesChannel, { id: before.id }, {}, { tenantId: before.tenantId, organizationId: before.organizationId });
674
674
  if (!record) {
675
675
  record = em.create(SalesChannel, channelSeedFromSnapshot(before));
676
676
  em.persist(record);
@@ -721,7 +721,7 @@ const deleteChannelCommand = {
721
721
  async execute(input, ctx) {
722
722
  const id = requireId(input, "Channel id is required");
723
723
  const em = ctx.container.resolve("em").fork();
724
- const record = await em.findOne(SalesChannel, { id });
724
+ const record = await findOneWithDecryption(em, SalesChannel, { id }, {});
725
725
  if (!record) throw new CrudHttpError(404, { error: "Channel not found" });
726
726
  ensureTenantScope(ctx, record.tenantId);
727
727
  ensureOrganizationScope(ctx, record.organizationId);
@@ -765,7 +765,7 @@ const deleteChannelCommand = {
765
765
  const before = payload?.before;
766
766
  if (!before) return;
767
767
  const em = ctx.container.resolve("em").fork();
768
- let record = await em.findOne(SalesChannel, { id: before.id });
768
+ let record = await findOneWithDecryption(em, SalesChannel, { id: before.id }, {}, { tenantId: before.tenantId, organizationId: before.organizationId });
769
769
  if (!record) {
770
770
  record = em.create(SalesChannel, channelSeedFromSnapshot(before));
771
771
  em.persist(record);
@@ -1615,7 +1615,7 @@ const createTaxRateCommand = {
1615
1615
  ensureOrganizationScope(ctx, parsed.organizationId);
1616
1616
  const em = ctx.container.resolve("em").fork();
1617
1617
  if (parsed.channelId) {
1618
- const channel = await em.findOne(SalesChannel, { id: parsed.channelId, deletedAt: null });
1618
+ const channel = await findOneWithDecryption(em, SalesChannel, { id: parsed.channelId, deletedAt: null }, {}, { tenantId: parsed.tenantId, organizationId: parsed.organizationId });
1619
1619
  const channelInScope = assertFound(channel, "Channel not found for tax rate");
1620
1620
  ensureSameScope(channelInScope, parsed.organizationId, parsed.tenantId);
1621
1621
  }
@@ -1745,7 +1745,7 @@ const updateTaxRateCommand = {
1745
1745
  record.organizationId = scope.organizationId;
1746
1746
  record.tenantId = scope.tenantId;
1747
1747
  if (parsed.channelId !== void 0 && parsed.channelId !== null) {
1748
- const channel = await em.findOne(SalesChannel, { id: parsed.channelId, deletedAt: null });
1748
+ const channel = await findOneWithDecryption(em, SalesChannel, { id: parsed.channelId, deletedAt: null }, {}, { tenantId: record.tenantId, organizationId: record.organizationId });
1749
1749
  const channelInScope = assertFound(channel, "Channel not found for tax rate");
1750
1750
  ensureSameScope(channelInScope, record.organizationId, record.tenantId);
1751
1751
  }