@open-mercato/shared 0.6.5-develop.4477.1.7a250f91b8 → 0.6.5-develop.4498.1.55dc06a57c

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.
@@ -103,6 +103,38 @@ class TenantEncryptionSubscriber {
103
103
  helper.__originalEntityData = snapshot;
104
104
  helper.__touched = false;
105
105
  }
106
+ /**
107
+ * Reports whether a managed entity currently carries un-flushed changes relative to its load
108
+ * baseline, using MikroORM's own comparator so the verdict matches the change-set computer that
109
+ * runs at flush time. Used to avoid re-baselining (and thereby discarding) pending writes when a
110
+ * decrypt pass traverses back into an entity a command already mutated.
111
+ */
112
+ hasPendingChanges(target, meta, em) {
113
+ const helper = target?.__helper;
114
+ if (!helper || typeof helper !== "object") return false;
115
+ const original = helper.__originalEntityData;
116
+ if (!original) return false;
117
+ const entityName = meta?.className || meta?.name;
118
+ try {
119
+ const comparator = em?.getComparator?.();
120
+ if (entityName && comparator?.prepareEntity) {
121
+ const current = comparator.prepareEntity(target);
122
+ if (typeof comparator.matching === "function") {
123
+ return !comparator.matching(entityName, original, current);
124
+ }
125
+ if (typeof comparator.diffEntities === "function") {
126
+ const diff = comparator.diffEntities(entityName, original, current);
127
+ return !!diff && Object.keys(diff).length > 0;
128
+ }
129
+ }
130
+ } catch (err) {
131
+ debug("\u26AA\uFE0F subscriber.pending_changes.compare_failed", {
132
+ entity: entityName,
133
+ message: err?.message ?? String(err)
134
+ });
135
+ }
136
+ return helper.__touched === true;
137
+ }
106
138
  async encrypt(target, meta, em, changeSet) {
107
139
  if (!isTenantDataEncryptionEnabled() || !this.service.isEnabled()) {
108
140
  debug("\u26AA\uFE0F subscriber.skip", { reason: "disabled", entity: meta?.className || meta?.name });
@@ -203,9 +235,10 @@ class TenantEncryptionSubscriber {
203
235
  debug("\u26AA\uFE0F subscriber.skip", { reason: "no-tenant", entityId });
204
236
  return;
205
237
  }
238
+ const hadPendingChanges = syncOriginal ? this.hasPendingChanges(target, resolvedMeta, em) : false;
206
239
  const decrypted = await this.service.decryptEntityPayload(entityId, target, scopedTenantId, scopedOrgId);
207
240
  Object.assign(target, decrypted);
208
- if (syncOriginal) {
241
+ if (syncOriginal && !hadPendingChanges) {
209
242
  this.syncOriginalEntityData(target, resolvedMeta, em);
210
243
  }
211
244
  const nextFallback = fallbackScope ?? (tenantId || organizationId ? { tenantId: tenantId ?? null, organizationId: organizationId ?? null } : { tenantId: scopedTenantId, organizationId: scopedOrgId });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/encryption/subscriber.ts"],
4
- "sourcesContent": ["import type { EntityMetadata, EventArgs, EventSubscriber } from '@mikro-orm/core'\nimport { ReferenceKind } from '@mikro-orm/core'\nimport { resolveEntityIdFromMetadata } from './entityIds'\nimport { TenantDataEncryptionService } from './tenantDataEncryptionService'\nimport { isTenantDataEncryptionEnabled } from './toggles'\nimport { isEncryptionDebugEnabled } from './toggles'\nimport { resolveTenantEncryptionService } from './customFieldValues'\n\ntype Scoped = {\n tenantId?: string | null\n tenant_id?: string | null\n tenant?: { id?: string | null } | null\n organizationId?: string | null\n organization_id?: string | null\n organization?: { id?: string | null } | null\n}\n\ntype Scope = { tenantId: string | null; organizationId: string | null }\n\nfunction resolveScope(entity: Scoped): Scope {\n const tenantId = entity.tenantId ?? entity.tenant_id ?? entity.tenant?.id ?? null\n const organizationId = entity.organizationId ?? entity.organization_id ?? entity.organization?.id ?? null\n return {\n tenantId: tenantId ? String(tenantId) : null,\n organizationId: organizationId ? String(organizationId) : null,\n }\n}\n\nfunction debug(event: string, payload: Record<string, unknown>) {\n if (!isEncryptionDebugEnabled()) return\n try {\n // eslint-disable-next-line no-console\n console.debug(event, payload)\n } catch {\n // ignore\n }\n}\n\nconst registeredEventManagers = new WeakSet<object>()\n\nconst subscribersByService = new WeakMap<TenantDataEncryptionService, TenantEncryptionSubscriber>()\n\nfunction getSubscriberForService(service: TenantDataEncryptionService): TenantEncryptionSubscriber {\n const existing = subscribersByService.get(service)\n if (existing) return existing\n const subscriber = new TenantEncryptionSubscriber(service)\n subscribersByService.set(service, subscriber)\n return subscriber\n}\n\nconst toSnakeCase = (value: string): string =>\n value.replace(/([A-Z])/g, '_$1').replace(/__/g, '_').toLowerCase()\n\nexport class TenantEncryptionSubscriber implements EventSubscriber<any> {\n constructor(private readonly service: TenantDataEncryptionService) {}\n\n getSubscribedEntities() {\n return [] // listen to all entities\n }\n\n private resolveMeta(\n meta: EntityMetadata<any> | undefined,\n entity: Record<string, unknown>,\n em?: { getMetadata?: () => any },\n ): EntityMetadata<any> | undefined {\n if (meta) return meta\n const ctor = (entity as any)?.constructor\n const name = ctor?.name\n const registry = em?.getMetadata?.()\n if (!registry || !name) return meta\n try { return registry.find?.(name) } catch {}\n try { return registry.find?.(ctor) } catch {}\n try { return registry.get?.(name) } catch {}\n try { return registry.get?.(ctor) } catch {}\n const all =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray((registry as any).metadata) ? (registry as any).metadata : undefined) ||\n (registry as any).metadata ||\n {}\n try {\n const entries = Array.isArray(all) ? all : Object.values<any>(all)\n const match = entries.find(\n (m: any) =>\n m?.className === name ||\n m?.name === name ||\n m?.entityName === name ||\n m?.collection === ctor?.prototype?.__meta?.tableName ||\n m?.tableName === ctor?.prototype?.__meta?.tableName,\n )\n if (match) return match as EntityMetadata<any>\n } catch {\n // best-effort\n }\n return meta\n }\n\n private resolveEntityId(meta: EntityMetadata<any> | undefined): string | null {\n try {\n return resolveEntityIdFromMetadata(meta)\n } catch {\n return null\n }\n }\n\n private syncOriginalEntityData(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n em?: { getComparator?: () => any },\n ) {\n const helper = (target as any)?.__helper\n if (!helper || typeof helper !== 'object') return\n\n // Prefer MikroORM comparator snapshot so change detection uses the expected shape.\n try {\n const comparator = em?.getComparator?.()\n if (comparator?.prepareEntity) {\n helper.__originalEntityData = comparator.prepareEntity(target)\n helper.__touched = false\n return\n }\n } catch (err) {\n debug('\u26AA\uFE0F subscriber.sync_original.comparator_failed', {\n entity: meta?.className || meta?.name,\n message: (err as Error)?.message ?? String(err),\n })\n }\n\n // Fallback: shallow snapshot of scalar/owner props to keep entities clean without comparator.\n const properties = meta?.properties ? Object.values(meta.properties) : []\n if (properties.length === 0) return\n const snapshot: Record<string, unknown> = { ...(helper.__originalEntityData ?? {}) }\n for (const prop of properties) {\n if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes((prop as any).kind)) continue\n const name = (prop as any).name\n if (typeof name !== 'string' || !name.length) continue\n snapshot[name] = (target as Record<string, unknown>)[name]\n }\n helper.__originalEntityData = snapshot\n helper.__touched = false\n }\n\n private async encrypt(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n em?: { getMetadata?: () => any; getComparator?: () => any },\n changeSet?: { payload?: Record<string, unknown> },\n ) {\n if (!isTenantDataEncryptionEnabled() || !this.service.isEnabled()) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'disabled', entity: meta?.className || meta?.name })\n return\n }\n const resolvedMeta = this.resolveMeta(meta, target, em)\n const entityId = this.resolveEntityId(resolvedMeta)\n if (!entityId) {\n debug('\u26A0\uFE0F subscriber.decrypt.skip.entity_id_missing', {\n metaName: resolvedMeta?.className || resolvedMeta?.name,\n table: (resolvedMeta as any)?.tableName,\n })\n return\n }\n const { tenantId, organizationId } = resolveScope(target)\n if (!tenantId) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'no-tenant', entityId })\n return\n }\n const encrypted = await this.service.encryptEntityPayload(entityId, target, tenantId, organizationId)\n const metaProps: Record<string, unknown> = resolvedMeta?.properties && typeof resolvedMeta.properties === 'object'\n ? resolvedMeta.properties\n : {}\n const payloadObj: Record<string, unknown> | null =\n changeSet && typeof changeSet === 'object'\n ? (typeof changeSet.payload === 'object' && changeSet.payload\n ? (changeSet.payload as Record<string, unknown>)\n : ((changeSet.payload = {}) as Record<string, unknown>))\n : null\n const updates: Record<string, unknown> = {}\n const columnNameFor = (propKey: string, prop: Record<string, unknown> | undefined): string => {\n try {\n if (prop && typeof prop === 'object') {\n const explicit = (prop as any)?.fieldName\n if (typeof explicit === 'string' && explicit.length) return explicit\n const name = (prop as any)?.name\n if (typeof name === 'string' && name.length) return name\n }\n } catch (err) {\n debug('\u26A0\uFE0F subscriber.column_name.resolve', {\n entityId,\n propKey,\n message: (err as Error)?.message ?? String(err),\n })\n }\n return toSnakeCase(propKey)\n }\n\n for (const [key, value] of Object.entries(encrypted)) {\n const prop = (metaProps as Record<string, any>)[key]\n if (!prop || typeof prop !== 'object') continue\n if ((target as Record<string, unknown>)[key] === value) continue\n updates[key] = value\n }\n if (Object.keys(updates).length === 0) return\n Object.assign(target, updates)\n if (payloadObj) {\n try {\n const ensureColumnKey = (propKey: string, value: unknown) => {\n const columnName = columnNameFor(propKey, (metaProps as Record<string, any>)[propKey])\n const canonicalKey = columnName || toSnakeCase(propKey)\n const aliases = new Set(\n [propKey, toSnakeCase(propKey), columnName, columnName ? toSnakeCase(columnName) : undefined].filter(\n (v): v is string => typeof v === 'string' && v.length > 0,\n ),\n )\n for (const alias of aliases) {\n if (Object.prototype.hasOwnProperty.call(payloadObj, alias)) delete payloadObj[alias]\n }\n const finalKey = columnName || toSnakeCase(propKey)\n payloadObj[finalKey] = value\n }\n for (const key of Object.keys(updates)) {\n ensureColumnKey(key, updates[key])\n }\n } catch (err) {\n debug('\u26A0\uFE0F subscriber.payload.normalize.error', {\n entityId,\n message: (err as Error)?.message ?? String(err),\n })\n }\n }\n }\n\n async decryptEntityGraph(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n em?: { getMetadata?: () => any; getComparator?: () => any },\n opts: { syncOriginal?: boolean; seen?: WeakSet<object>; fallbackScope?: Scope } = {},\n ) {\n await this.decrypt(target, meta, em, opts)\n }\n\n private async decrypt(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n em?: { getMetadata?: () => any; getComparator?: () => any },\n {\n syncOriginal = false,\n seen,\n fallbackScope,\n }: { syncOriginal?: boolean; seen?: WeakSet<object>; fallbackScope?: Scope } = {},\n ) {\n const visited = seen ?? new WeakSet<object>()\n if (visited.has(target as object)) return\n visited.add(target as object)\n if (!isTenantDataEncryptionEnabled() || !this.service.isEnabled()) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'disabled', entity: meta?.className || meta?.name })\n return\n }\n const resolvedMeta = this.resolveMeta(meta, target, em)\n const entityId = this.resolveEntityId(resolvedMeta)\n if (!entityId) return\n const { tenantId, organizationId } = resolveScope(target)\n const scopedTenantId = tenantId ?? fallbackScope?.tenantId ?? null\n const scopedOrgId = organizationId ?? fallbackScope?.organizationId ?? null\n if (!scopedTenantId) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'no-tenant', entityId })\n return\n }\n const decrypted = await this.service.decryptEntityPayload(entityId, target, scopedTenantId, scopedOrgId)\n Object.assign(target, decrypted)\n if (syncOriginal) {\n this.syncOriginalEntityData(target, resolvedMeta, em as any)\n }\n const nextFallback =\n fallbackScope ??\n (tenantId || organizationId\n ? { tenantId: tenantId ?? null, organizationId: organizationId ?? null }\n : { tenantId: scopedTenantId, organizationId: scopedOrgId })\n // Best-effort deep decrypt for loaded relations so populated graphs get cleaned too.\n try {\n const extractEntities = (value: any): any[] => {\n if (!value) return []\n // MikroORM Reference wrapper\n if (typeof value === 'object' && typeof (value as any).isInitialized === 'function') {\n try {\n if ((value as any).isInitialized()) {\n const unwrapped = typeof (value as any).unwrap === 'function' ? (value as any).unwrap() : (value as any).__entity ?? (value as any)\n if (unwrapped && typeof unwrapped === 'object') return [unwrapped]\n }\n } catch {\n // ignore\n }\n return []\n }\n // Collection wrapper\n if (typeof value === 'object' && typeof (value as any).isInitialized === 'function' && typeof (value as any).getItems === 'function') {\n try {\n return (value as any).isInitialized() ? (value as any).getItems() ?? [] : []\n } catch {\n return []\n }\n }\n if (Array.isArray(value)) return value\n if (typeof value === 'object') return [value]\n return []\n }\n const props = resolvedMeta?.properties ? Object.values(resolvedMeta.properties) : []\n for (const prop of props) {\n const kind = (prop as any)?.kind\n const name = (prop as any)?.name\n if (typeof name !== 'string' || !name.length) continue\n const value = (target as any)[name]\n if (!value) continue\n // Single-valued relation\n if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(kind)) {\n const nestedEntities = extractEntities(value)\n for (const nested of nestedEntities) {\n const nestedMeta = this.resolveMeta((nested as any).__meta ?? (nested as any).__helper?.__meta, nested, em)\n await this.decrypt(nested as Record<string, unknown>, nestedMeta, em, {\n syncOriginal: true,\n seen: visited,\n fallbackScope: nextFallback,\n })\n }\n continue\n }\n // Collections\n if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(kind)) {\n const items = extractEntities(value)\n for (const item of items) {\n if (!item || typeof item !== 'object') continue\n const nestedMeta = this.resolveMeta((item as any).__meta ?? (item as any).__helper?.__meta, item, em)\n await this.decrypt(item as Record<string, unknown>, nestedMeta, em, {\n syncOriginal: true,\n seen: visited,\n fallbackScope: nextFallback,\n })\n }\n }\n }\n } catch (err) {\n debug('\u26A0\uFE0F subscriber.deep_decrypt.error', {\n entityId,\n message: (err as Error)?.message ?? String(err),\n })\n }\n }\n\n async beforeCreate(args: EventArgs<any>) {\n await this.encrypt(args.entity as Record<string, unknown>, args.meta, args.em, args.changeSet as any)\n }\n\n async beforeUpdate(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em)\n await this.encrypt(args.entity as Record<string, unknown>, args.meta, args.em, args.changeSet as any)\n }\n\n async afterCreate(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n\n async afterUpdate(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n\n async afterUpsert(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n\n async onLoad(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n\n async afterFind(args: EventArgs<any> & { entities?: unknown[] }) {\n const entities = Array.isArray(args.entities) ? args.entities : []\n for (const entity of entities) {\n await this.decrypt(entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n }\n}\n\nexport function registerTenantEncryptionSubscriber(\n em: { getEventManager?: () => { registerSubscriber?: (subscriber: EventSubscriber<any>) => void } } | null | undefined,\n service: TenantDataEncryptionService,\n): void {\n const eventManager = em?.getEventManager?.()\n if (!eventManager || typeof eventManager.registerSubscriber !== 'function') return\n if (registeredEventManagers.has(eventManager)) return\n eventManager.registerSubscriber(new TenantEncryptionSubscriber(service))\n registeredEventManagers.add(eventManager)\n}\n\nexport async function decryptEntitiesWithFallbackScope(\n targets: unknown | unknown[],\n {\n em,\n tenantId,\n organizationId,\n encryptionService,\n }: {\n em: { getMetadata?: () => any; getComparator?: () => any }\n tenantId?: string | null\n organizationId?: string | null\n encryptionService?: TenantDataEncryptionService | null\n },\n): Promise<void> {\n if (!isTenantDataEncryptionEnabled()) return\n const list = Array.isArray(targets) ? targets : [targets]\n if (!list.length) return\n const service = encryptionService ?? resolveTenantEncryptionService(em as any)\n if (!service || !service.isEnabled()) return\n const subscriber = getSubscriberForService(service)\n const fallback: Scope | undefined =\n tenantId || organizationId\n ? {\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n }\n : undefined\n for (const entity of list) {\n if (!entity || typeof entity !== 'object') continue\n const meta = (entity as any).__meta ?? (entity as any).__helper?.__meta\n await subscriber.decryptEntityGraph(entity as Record<string, unknown>, meta, em as any, {\n syncOriginal: true,\n fallbackScope: fallback,\n })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityMetadata, EventArgs, EventSubscriber } from '@mikro-orm/core'\nimport { ReferenceKind } from '@mikro-orm/core'\nimport { resolveEntityIdFromMetadata } from './entityIds'\nimport { TenantDataEncryptionService } from './tenantDataEncryptionService'\nimport { isTenantDataEncryptionEnabled } from './toggles'\nimport { isEncryptionDebugEnabled } from './toggles'\nimport { resolveTenantEncryptionService } from './customFieldValues'\n\ntype Scoped = {\n tenantId?: string | null\n tenant_id?: string | null\n tenant?: { id?: string | null } | null\n organizationId?: string | null\n organization_id?: string | null\n organization?: { id?: string | null } | null\n}\n\ntype Scope = { tenantId: string | null; organizationId: string | null }\n\nfunction resolveScope(entity: Scoped): Scope {\n const tenantId = entity.tenantId ?? entity.tenant_id ?? entity.tenant?.id ?? null\n const organizationId = entity.organizationId ?? entity.organization_id ?? entity.organization?.id ?? null\n return {\n tenantId: tenantId ? String(tenantId) : null,\n organizationId: organizationId ? String(organizationId) : null,\n }\n}\n\nfunction debug(event: string, payload: Record<string, unknown>) {\n if (!isEncryptionDebugEnabled()) return\n try {\n // eslint-disable-next-line no-console\n console.debug(event, payload)\n } catch {\n // ignore\n }\n}\n\nconst registeredEventManagers = new WeakSet<object>()\n\nconst subscribersByService = new WeakMap<TenantDataEncryptionService, TenantEncryptionSubscriber>()\n\nfunction getSubscriberForService(service: TenantDataEncryptionService): TenantEncryptionSubscriber {\n const existing = subscribersByService.get(service)\n if (existing) return existing\n const subscriber = new TenantEncryptionSubscriber(service)\n subscribersByService.set(service, subscriber)\n return subscriber\n}\n\nconst toSnakeCase = (value: string): string =>\n value.replace(/([A-Z])/g, '_$1').replace(/__/g, '_').toLowerCase()\n\nexport class TenantEncryptionSubscriber implements EventSubscriber<any> {\n constructor(private readonly service: TenantDataEncryptionService) {}\n\n getSubscribedEntities() {\n return [] // listen to all entities\n }\n\n private resolveMeta(\n meta: EntityMetadata<any> | undefined,\n entity: Record<string, unknown>,\n em?: { getMetadata?: () => any },\n ): EntityMetadata<any> | undefined {\n if (meta) return meta\n const ctor = (entity as any)?.constructor\n const name = ctor?.name\n const registry = em?.getMetadata?.()\n if (!registry || !name) return meta\n try { return registry.find?.(name) } catch {}\n try { return registry.find?.(ctor) } catch {}\n try { return registry.get?.(name) } catch {}\n try { return registry.get?.(ctor) } catch {}\n const all =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray((registry as any).metadata) ? (registry as any).metadata : undefined) ||\n (registry as any).metadata ||\n {}\n try {\n const entries = Array.isArray(all) ? all : Object.values<any>(all)\n const match = entries.find(\n (m: any) =>\n m?.className === name ||\n m?.name === name ||\n m?.entityName === name ||\n m?.collection === ctor?.prototype?.__meta?.tableName ||\n m?.tableName === ctor?.prototype?.__meta?.tableName,\n )\n if (match) return match as EntityMetadata<any>\n } catch {\n // best-effort\n }\n return meta\n }\n\n private resolveEntityId(meta: EntityMetadata<any> | undefined): string | null {\n try {\n return resolveEntityIdFromMetadata(meta)\n } catch {\n return null\n }\n }\n\n private syncOriginalEntityData(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n em?: { getComparator?: () => any },\n ) {\n const helper = (target as any)?.__helper\n if (!helper || typeof helper !== 'object') return\n\n // Prefer MikroORM comparator snapshot so change detection uses the expected shape.\n try {\n const comparator = em?.getComparator?.()\n if (comparator?.prepareEntity) {\n helper.__originalEntityData = comparator.prepareEntity(target)\n helper.__touched = false\n return\n }\n } catch (err) {\n debug('\u26AA\uFE0F subscriber.sync_original.comparator_failed', {\n entity: meta?.className || meta?.name,\n message: (err as Error)?.message ?? String(err),\n })\n }\n\n // Fallback: shallow snapshot of scalar/owner props to keep entities clean without comparator.\n const properties = meta?.properties ? Object.values(meta.properties) : []\n if (properties.length === 0) return\n const snapshot: Record<string, unknown> = { ...(helper.__originalEntityData ?? {}) }\n for (const prop of properties) {\n if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes((prop as any).kind)) continue\n const name = (prop as any).name\n if (typeof name !== 'string' || !name.length) continue\n snapshot[name] = (target as Record<string, unknown>)[name]\n }\n helper.__originalEntityData = snapshot\n helper.__touched = false\n }\n\n /**\n * Reports whether a managed entity currently carries un-flushed changes relative to its load\n * baseline, using MikroORM's own comparator so the verdict matches the change-set computer that\n * runs at flush time. Used to avoid re-baselining (and thereby discarding) pending writes when a\n * decrypt pass traverses back into an entity a command already mutated.\n */\n private hasPendingChanges(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n em?: { getComparator?: () => any },\n ): boolean {\n const helper = (target as any)?.__helper\n if (!helper || typeof helper !== 'object') return false\n const original = helper.__originalEntityData\n if (!original) return false\n const entityName = meta?.className || meta?.name\n try {\n const comparator = em?.getComparator?.()\n if (entityName && comparator?.prepareEntity) {\n const current = comparator.prepareEntity(target)\n if (typeof comparator.matching === 'function') {\n return !comparator.matching(entityName, original, current)\n }\n if (typeof comparator.diffEntities === 'function') {\n const diff = comparator.diffEntities(entityName, original, current)\n return !!diff && Object.keys(diff).length > 0\n }\n }\n } catch (err) {\n debug('\u26AA\uFE0F subscriber.pending_changes.compare_failed', {\n entity: entityName,\n message: (err as Error)?.message ?? String(err),\n })\n }\n return helper.__touched === true\n }\n\n private async encrypt(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n em?: { getMetadata?: () => any; getComparator?: () => any },\n changeSet?: { payload?: Record<string, unknown> },\n ) {\n if (!isTenantDataEncryptionEnabled() || !this.service.isEnabled()) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'disabled', entity: meta?.className || meta?.name })\n return\n }\n const resolvedMeta = this.resolveMeta(meta, target, em)\n const entityId = this.resolveEntityId(resolvedMeta)\n if (!entityId) {\n debug('\u26A0\uFE0F subscriber.decrypt.skip.entity_id_missing', {\n metaName: resolvedMeta?.className || resolvedMeta?.name,\n table: (resolvedMeta as any)?.tableName,\n })\n return\n }\n const { tenantId, organizationId } = resolveScope(target)\n if (!tenantId) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'no-tenant', entityId })\n return\n }\n const encrypted = await this.service.encryptEntityPayload(entityId, target, tenantId, organizationId)\n const metaProps: Record<string, unknown> = resolvedMeta?.properties && typeof resolvedMeta.properties === 'object'\n ? resolvedMeta.properties\n : {}\n const payloadObj: Record<string, unknown> | null =\n changeSet && typeof changeSet === 'object'\n ? (typeof changeSet.payload === 'object' && changeSet.payload\n ? (changeSet.payload as Record<string, unknown>)\n : ((changeSet.payload = {}) as Record<string, unknown>))\n : null\n const updates: Record<string, unknown> = {}\n const columnNameFor = (propKey: string, prop: Record<string, unknown> | undefined): string => {\n try {\n if (prop && typeof prop === 'object') {\n const explicit = (prop as any)?.fieldName\n if (typeof explicit === 'string' && explicit.length) return explicit\n const name = (prop as any)?.name\n if (typeof name === 'string' && name.length) return name\n }\n } catch (err) {\n debug('\u26A0\uFE0F subscriber.column_name.resolve', {\n entityId,\n propKey,\n message: (err as Error)?.message ?? String(err),\n })\n }\n return toSnakeCase(propKey)\n }\n\n for (const [key, value] of Object.entries(encrypted)) {\n const prop = (metaProps as Record<string, any>)[key]\n if (!prop || typeof prop !== 'object') continue\n if ((target as Record<string, unknown>)[key] === value) continue\n updates[key] = value\n }\n if (Object.keys(updates).length === 0) return\n Object.assign(target, updates)\n if (payloadObj) {\n try {\n const ensureColumnKey = (propKey: string, value: unknown) => {\n const columnName = columnNameFor(propKey, (metaProps as Record<string, any>)[propKey])\n const canonicalKey = columnName || toSnakeCase(propKey)\n const aliases = new Set(\n [propKey, toSnakeCase(propKey), columnName, columnName ? toSnakeCase(columnName) : undefined].filter(\n (v): v is string => typeof v === 'string' && v.length > 0,\n ),\n )\n for (const alias of aliases) {\n if (Object.prototype.hasOwnProperty.call(payloadObj, alias)) delete payloadObj[alias]\n }\n const finalKey = columnName || toSnakeCase(propKey)\n payloadObj[finalKey] = value\n }\n for (const key of Object.keys(updates)) {\n ensureColumnKey(key, updates[key])\n }\n } catch (err) {\n debug('\u26A0\uFE0F subscriber.payload.normalize.error', {\n entityId,\n message: (err as Error)?.message ?? String(err),\n })\n }\n }\n }\n\n async decryptEntityGraph(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n em?: { getMetadata?: () => any; getComparator?: () => any },\n opts: { syncOriginal?: boolean; seen?: WeakSet<object>; fallbackScope?: Scope } = {},\n ) {\n await this.decrypt(target, meta, em, opts)\n }\n\n private async decrypt(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n em?: { getMetadata?: () => any; getComparator?: () => any },\n {\n syncOriginal = false,\n seen,\n fallbackScope,\n }: { syncOriginal?: boolean; seen?: WeakSet<object>; fallbackScope?: Scope } = {},\n ) {\n const visited = seen ?? new WeakSet<object>()\n if (visited.has(target as object)) return\n visited.add(target as object)\n if (!isTenantDataEncryptionEnabled() || !this.service.isEnabled()) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'disabled', entity: meta?.className || meta?.name })\n return\n }\n const resolvedMeta = this.resolveMeta(meta, target, em)\n const entityId = this.resolveEntityId(resolvedMeta)\n if (!entityId) return\n const { tenantId, organizationId } = resolveScope(target)\n const scopedTenantId = tenantId ?? fallbackScope?.tenantId ?? null\n const scopedOrgId = organizationId ?? fallbackScope?.organizationId ?? null\n if (!scopedTenantId) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'no-tenant', entityId })\n return\n }\n // Capture pending (un-flushed) changes BEFORE decrypt mutates the target. Re-baselining a\n // managed entity that a command already mutated would clear its dirty changeset and silently\n // drop the pending write (e.g. an undo handler that mutates an entity, then loads a related\n // encrypted entity whose deep-decrypt recurses back into the still-dirty entity before flush).\n const hadPendingChanges = syncOriginal ? this.hasPendingChanges(target, resolvedMeta, em as any) : false\n const decrypted = await this.service.decryptEntityPayload(entityId, target, scopedTenantId, scopedOrgId)\n Object.assign(target, decrypted)\n if (syncOriginal && !hadPendingChanges) {\n this.syncOriginalEntityData(target, resolvedMeta, em as any)\n }\n const nextFallback =\n fallbackScope ??\n (tenantId || organizationId\n ? { tenantId: tenantId ?? null, organizationId: organizationId ?? null }\n : { tenantId: scopedTenantId, organizationId: scopedOrgId })\n // Best-effort deep decrypt for loaded relations so populated graphs get cleaned too.\n try {\n const extractEntities = (value: any): any[] => {\n if (!value) return []\n // MikroORM Reference wrapper\n if (typeof value === 'object' && typeof (value as any).isInitialized === 'function') {\n try {\n if ((value as any).isInitialized()) {\n const unwrapped = typeof (value as any).unwrap === 'function' ? (value as any).unwrap() : (value as any).__entity ?? (value as any)\n if (unwrapped && typeof unwrapped === 'object') return [unwrapped]\n }\n } catch {\n // ignore\n }\n return []\n }\n // Collection wrapper\n if (typeof value === 'object' && typeof (value as any).isInitialized === 'function' && typeof (value as any).getItems === 'function') {\n try {\n return (value as any).isInitialized() ? (value as any).getItems() ?? [] : []\n } catch {\n return []\n }\n }\n if (Array.isArray(value)) return value\n if (typeof value === 'object') return [value]\n return []\n }\n const props = resolvedMeta?.properties ? Object.values(resolvedMeta.properties) : []\n for (const prop of props) {\n const kind = (prop as any)?.kind\n const name = (prop as any)?.name\n if (typeof name !== 'string' || !name.length) continue\n const value = (target as any)[name]\n if (!value) continue\n // Single-valued relation\n if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(kind)) {\n const nestedEntities = extractEntities(value)\n for (const nested of nestedEntities) {\n const nestedMeta = this.resolveMeta((nested as any).__meta ?? (nested as any).__helper?.__meta, nested, em)\n await this.decrypt(nested as Record<string, unknown>, nestedMeta, em, {\n syncOriginal: true,\n seen: visited,\n fallbackScope: nextFallback,\n })\n }\n continue\n }\n // Collections\n if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(kind)) {\n const items = extractEntities(value)\n for (const item of items) {\n if (!item || typeof item !== 'object') continue\n const nestedMeta = this.resolveMeta((item as any).__meta ?? (item as any).__helper?.__meta, item, em)\n await this.decrypt(item as Record<string, unknown>, nestedMeta, em, {\n syncOriginal: true,\n seen: visited,\n fallbackScope: nextFallback,\n })\n }\n }\n }\n } catch (err) {\n debug('\u26A0\uFE0F subscriber.deep_decrypt.error', {\n entityId,\n message: (err as Error)?.message ?? String(err),\n })\n }\n }\n\n async beforeCreate(args: EventArgs<any>) {\n await this.encrypt(args.entity as Record<string, unknown>, args.meta, args.em, args.changeSet as any)\n }\n\n async beforeUpdate(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em)\n await this.encrypt(args.entity as Record<string, unknown>, args.meta, args.em, args.changeSet as any)\n }\n\n async afterCreate(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n\n async afterUpdate(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n\n async afterUpsert(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n\n async onLoad(args: EventArgs<any>) {\n await this.decrypt(args.entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n\n async afterFind(args: EventArgs<any> & { entities?: unknown[] }) {\n const entities = Array.isArray(args.entities) ? args.entities : []\n for (const entity of entities) {\n await this.decrypt(entity as Record<string, unknown>, args.meta, args.em, { syncOriginal: true })\n }\n }\n}\n\nexport function registerTenantEncryptionSubscriber(\n em: { getEventManager?: () => { registerSubscriber?: (subscriber: EventSubscriber<any>) => void } } | null | undefined,\n service: TenantDataEncryptionService,\n): void {\n const eventManager = em?.getEventManager?.()\n if (!eventManager || typeof eventManager.registerSubscriber !== 'function') return\n if (registeredEventManagers.has(eventManager)) return\n eventManager.registerSubscriber(new TenantEncryptionSubscriber(service))\n registeredEventManagers.add(eventManager)\n}\n\nexport async function decryptEntitiesWithFallbackScope(\n targets: unknown | unknown[],\n {\n em,\n tenantId,\n organizationId,\n encryptionService,\n }: {\n em: { getMetadata?: () => any; getComparator?: () => any }\n tenantId?: string | null\n organizationId?: string | null\n encryptionService?: TenantDataEncryptionService | null\n },\n): Promise<void> {\n if (!isTenantDataEncryptionEnabled()) return\n const list = Array.isArray(targets) ? targets : [targets]\n if (!list.length) return\n const service = encryptionService ?? resolveTenantEncryptionService(em as any)\n if (!service || !service.isEnabled()) return\n const subscriber = getSubscriberForService(service)\n const fallback: Scope | undefined =\n tenantId || organizationId\n ? {\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n }\n : undefined\n for (const entity of list) {\n if (!entity || typeof entity !== 'object') continue\n const meta = (entity as any).__meta ?? (entity as any).__helper?.__meta\n await subscriber.decryptEntityGraph(entity as Record<string, unknown>, meta, em as any, {\n syncOriginal: true,\n fallbackScope: fallback,\n })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.5-develop.4477.1.7a250f91b8";
1
+ const APP_VERSION = "0.6.5-develop.4498.1.55dc06a57c";
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-develop.4477.1.7a250f91b8'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.5-develop.4498.1.55dc06a57c'\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-develop.4477.1.7a250f91b8",
3
+ "version": "0.6.5-develop.4498.1.55dc06a57c",
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-develop.4477.1.7a250f91b8",
95
+ "@open-mercato/cache": "0.6.5-develop.4498.1.55dc06a57c",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.1.0",
98
98
  "re2js": "2.8.3",
@@ -0,0 +1,96 @@
1
+ import { TenantEncryptionSubscriber } from '../subscriber'
2
+ import { registerEntityIds } from '../entityIds'
3
+ import type { TenantDataEncryptionService } from '../tenantDataEncryptionService'
4
+
5
+ // Regression coverage for issue #2498: the deep-decrypt re-baseline (syncOriginalEntityData) must
6
+ // NOT clear a managed entity's pending changes. When a command mutates an entity and then loads a
7
+ // related encrypted entity (whose deep-decrypt recurses back into the still-dirty entity) before
8
+ // the final flush, re-baselining the dirty entity silently dropped the pending write — the update
9
+ // command issued no UPDATE and `updated_at` never fired. The fix gates the re-baseline on the
10
+ // entity having no un-flushed changes (per MikroORM's own comparator).
11
+
12
+ type Helper = { __originalEntityData?: Record<string, unknown>; __touched?: boolean }
13
+
14
+ function makeComparator() {
15
+ return {
16
+ // Mirror MikroORM's prepared snapshot: a plain scalar copy of the entity.
17
+ prepareEntity(entity: Record<string, unknown>) {
18
+ const snapshot: Record<string, unknown> = {}
19
+ for (const [key, value] of Object.entries(entity)) {
20
+ if (key === '__helper' || key === '__meta') continue
21
+ snapshot[key] = value
22
+ }
23
+ return snapshot
24
+ },
25
+ // True when the two snapshots are identical (no pending changes). Order-independent, like
26
+ // MikroORM's real comparator (the production fix depends on that property-wise semantics).
27
+ matching(_entityName: string, a: Record<string, unknown>, b: Record<string, unknown>) {
28
+ const aKeys = Object.keys(a)
29
+ const bKeys = Object.keys(b)
30
+ if (aKeys.length !== bKeys.length) return false
31
+ return aKeys.every((key) => JSON.stringify(a[key]) === JSON.stringify(b[key]))
32
+ },
33
+ }
34
+ }
35
+
36
+ function makeEm() {
37
+ const comparator = makeComparator()
38
+ return { getComparator: () => comparator, getMetadata: () => undefined }
39
+ }
40
+
41
+ const META = { className: 'Thing', tableName: 'things', properties: {} } as any
42
+
43
+ describe('TenantEncryptionSubscriber change-tracking preservation (issue #2498)', () => {
44
+ const originalToggle = process.env.TENANT_DATA_ENCRYPTION
45
+
46
+ beforeEach(() => {
47
+ delete process.env.TENANT_DATA_ENCRYPTION // default => encryption enabled
48
+ registerEntityIds({ test: { thing: 'test:thing' } })
49
+ })
50
+
51
+ afterEach(() => {
52
+ if (originalToggle === undefined) delete process.env.TENANT_DATA_ENCRYPTION
53
+ else process.env.TENANT_DATA_ENCRYPTION = originalToggle
54
+ jest.restoreAllMocks()
55
+ })
56
+
57
+ function makeService(
58
+ decryptEntityPayload: (entityId: string, target: Record<string, unknown>) => Record<string, unknown> = () => ({}),
59
+ ): TenantDataEncryptionService {
60
+ return {
61
+ isEnabled: () => true,
62
+ async decryptEntityPayload(entityId: string, target: Record<string, unknown>) {
63
+ return decryptEntityPayload(entityId, target)
64
+ },
65
+ } as unknown as TenantDataEncryptionService
66
+ }
67
+
68
+ it('preserves pending scalar changes when re-baselining a dirty managed entity', async () => {
69
+ const helper: Helper = { __originalEntityData: { displayName: 'CHANGED', tenantId: 't1' }, __touched: true }
70
+ // Command restored the value in-memory but has not flushed yet.
71
+ const entity: Record<string, unknown> = { tenantId: 't1', displayName: 'Before', __helper: helper }
72
+
73
+ const subscriber = new TenantEncryptionSubscriber(makeService())
74
+ await subscriber.decryptEntityGraph(entity, META, makeEm(), { syncOriginal: true })
75
+
76
+ // Baseline must still reflect the un-restored value so the flush computes a non-empty changeset.
77
+ expect(helper.__originalEntityData).toEqual({ displayName: 'CHANGED', tenantId: 't1' })
78
+ expect(helper.__touched).toBe(true)
79
+ })
80
+
81
+ it('still re-baselines a clean entity so decrypted values are not re-persisted', async () => {
82
+ const helper: Helper = { __originalEntityData: { secret: 'enc:plain', tenantId: 't1' }, __touched: false }
83
+ const entity: Record<string, unknown> = { tenantId: 't1', secret: 'enc:plain', __helper: helper }
84
+
85
+ // Decrypt rewrites the ciphertext column to plaintext.
86
+ const subscriber = new TenantEncryptionSubscriber(
87
+ makeService(() => ({ secret: 'plain' })),
88
+ )
89
+ await subscriber.decryptEntityGraph(entity, META, makeEm(), { syncOriginal: true })
90
+
91
+ // Clean entity: re-baseline must snapshot the decrypted value so the next flush sees no change.
92
+ expect(entity.secret).toBe('plain')
93
+ expect(helper.__originalEntityData).toEqual({ secret: 'plain', tenantId: 't1' })
94
+ expect(helper.__touched).toBe(false)
95
+ })
96
+ })
@@ -139,6 +139,43 @@ export class TenantEncryptionSubscriber implements EventSubscriber<any> {
139
139
  helper.__touched = false
140
140
  }
141
141
 
142
+ /**
143
+ * Reports whether a managed entity currently carries un-flushed changes relative to its load
144
+ * baseline, using MikroORM's own comparator so the verdict matches the change-set computer that
145
+ * runs at flush time. Used to avoid re-baselining (and thereby discarding) pending writes when a
146
+ * decrypt pass traverses back into an entity a command already mutated.
147
+ */
148
+ private hasPendingChanges(
149
+ target: Record<string, unknown>,
150
+ meta: EntityMetadata<any> | undefined,
151
+ em?: { getComparator?: () => any },
152
+ ): boolean {
153
+ const helper = (target as any)?.__helper
154
+ if (!helper || typeof helper !== 'object') return false
155
+ const original = helper.__originalEntityData
156
+ if (!original) return false
157
+ const entityName = meta?.className || meta?.name
158
+ try {
159
+ const comparator = em?.getComparator?.()
160
+ if (entityName && comparator?.prepareEntity) {
161
+ const current = comparator.prepareEntity(target)
162
+ if (typeof comparator.matching === 'function') {
163
+ return !comparator.matching(entityName, original, current)
164
+ }
165
+ if (typeof comparator.diffEntities === 'function') {
166
+ const diff = comparator.diffEntities(entityName, original, current)
167
+ return !!diff && Object.keys(diff).length > 0
168
+ }
169
+ }
170
+ } catch (err) {
171
+ debug('⚪️ subscriber.pending_changes.compare_failed', {
172
+ entity: entityName,
173
+ message: (err as Error)?.message ?? String(err),
174
+ })
175
+ }
176
+ return helper.__touched === true
177
+ }
178
+
142
179
  private async encrypt(
143
180
  target: Record<string, unknown>,
144
181
  meta: EntityMetadata<any> | undefined,
@@ -264,9 +301,14 @@ export class TenantEncryptionSubscriber implements EventSubscriber<any> {
264
301
  debug('⚪️ subscriber.skip', { reason: 'no-tenant', entityId })
265
302
  return
266
303
  }
304
+ // Capture pending (un-flushed) changes BEFORE decrypt mutates the target. Re-baselining a
305
+ // managed entity that a command already mutated would clear its dirty changeset and silently
306
+ // drop the pending write (e.g. an undo handler that mutates an entity, then loads a related
307
+ // encrypted entity whose deep-decrypt recurses back into the still-dirty entity before flush).
308
+ const hadPendingChanges = syncOriginal ? this.hasPendingChanges(target, resolvedMeta, em as any) : false
267
309
  const decrypted = await this.service.decryptEntityPayload(entityId, target, scopedTenantId, scopedOrgId)
268
310
  Object.assign(target, decrypted)
269
- if (syncOriginal) {
311
+ if (syncOriginal && !hadPendingChanges) {
270
312
  this.syncOriginalEntityData(target, resolvedMeta, em as any)
271
313
  }
272
314
  const nextFallback =