@open-mercato/shared 0.10.1-develop.7679.1.cb2c13f49e → 0.10.1-develop.7687.1.e1a6f9de51

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.
Files changed (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/lib/crud/factory.js +3 -0
  3. package/dist/lib/crud/factory.js.map +2 -2
  4. package/dist/lib/encryption/decryptScope.js +51 -0
  5. package/dist/lib/encryption/decryptScope.js.map +7 -0
  6. package/dist/lib/encryption/find.js +24 -10
  7. package/dist/lib/encryption/find.js.map +2 -2
  8. package/dist/lib/encryption/subscriber.js +73 -15
  9. package/dist/lib/encryption/subscriber.js.map +2 -2
  10. package/dist/lib/query/encrypted-sort.js +15 -6
  11. package/dist/lib/query/encrypted-sort.js.map +2 -2
  12. package/dist/lib/query/engine.js +57 -10
  13. package/dist/lib/query/engine.js.map +2 -2
  14. package/dist/lib/query/types.js.map +1 -1
  15. package/dist/lib/version.js +1 -1
  16. package/dist/lib/version.js.map +1 -1
  17. package/package.json +2 -2
  18. package/src/lib/crud/__tests__/crud-factory.test.ts +16 -0
  19. package/src/lib/crud/factory.ts +13 -0
  20. package/src/lib/encryption/__tests__/decrypt-scope.test.ts +134 -0
  21. package/src/lib/encryption/__tests__/subscriber.onload-tenant-binding.test.ts +234 -0
  22. package/src/lib/encryption/__tests__/subscriber.refusal-aggregation.test.ts +105 -0
  23. package/src/lib/encryption/__tests__/subscriber.tenant-binding.test.ts +131 -0
  24. package/src/lib/encryption/decryptScope.ts +119 -0
  25. package/src/lib/encryption/find.ts +17 -12
  26. package/src/lib/encryption/subscriber.ts +101 -19
  27. package/src/lib/query/__tests__/engine-decrypt-refusal-log.test.ts +168 -0
  28. package/src/lib/query/__tests__/engine.test.ts +237 -0
  29. package/src/lib/query/encrypted-sort.ts +34 -8
  30. package/src/lib/query/engine.ts +70 -10
  31. package/src/lib/query/types.ts +15 -0
@@ -0,0 +1,51 @@
1
+ function resolveDecryptScope(input) {
2
+ const rowTenantId = input.rowTenantId ?? null;
3
+ const callerTenantId = input.callerTenantId ?? null;
4
+ if (rowTenantId && callerTenantId && rowTenantId !== callerTenantId) {
5
+ return { decrypt: false, reason: "tenant-mismatch", rowTenantId, callerTenantId };
6
+ }
7
+ return {
8
+ decrypt: true,
9
+ tenantId: rowTenantId ?? callerTenantId,
10
+ organizationId: input.rowOrganizationId ?? input.callerOrganizationId ?? null
11
+ };
12
+ }
13
+ function resolveDecryptEnabled(opts) {
14
+ return opts.decryptEncryptedFields !== false;
15
+ }
16
+ const MAX_SAMPLED_TENANT_IDS = 3;
17
+ const DECRYPT_REFUSAL_LOG_MESSAGE = "Skipped decryption for rows whose tenant does not match the query tenant";
18
+ const DECLINED_ENCRYPTED_SORT_LOG_MESSAGE = "Sorting on an encrypted field while decryption is declined; rows are ordered by ciphertext";
19
+ class DecryptRefusalTally {
20
+ constructor() {
21
+ this.refusedRowIds = /* @__PURE__ */ new Set();
22
+ this.unidentifiedCount = 0;
23
+ this.rowTenantIds = /* @__PURE__ */ new Set();
24
+ this.callerTenantId = null;
25
+ }
26
+ record(decision, rowId) {
27
+ if (rowId === void 0 || rowId === null || rowId === "") this.unidentifiedCount += 1;
28
+ else this.refusedRowIds.add(String(rowId));
29
+ this.callerTenantId = decision.callerTenantId;
30
+ if (this.rowTenantIds.size < MAX_SAMPLED_TENANT_IDS) this.rowTenantIds.add(decision.rowTenantId);
31
+ }
32
+ get refused() {
33
+ return this.refusedRowIds.size + this.unidentifiedCount;
34
+ }
35
+ toLogContext(entity) {
36
+ return {
37
+ entity,
38
+ refusedRows: this.refused,
39
+ callerTenantId: this.callerTenantId,
40
+ rowTenantIds: Array.from(this.rowTenantIds)
41
+ };
42
+ }
43
+ }
44
+ export {
45
+ DECLINED_ENCRYPTED_SORT_LOG_MESSAGE,
46
+ DECRYPT_REFUSAL_LOG_MESSAGE,
47
+ DecryptRefusalTally,
48
+ resolveDecryptEnabled,
49
+ resolveDecryptScope
50
+ };
51
+ //# sourceMappingURL=decryptScope.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/encryption/decryptScope.ts"],
4
+ "sourcesContent": ["export type DecryptScopeDecision =\n | { decrypt: true; tenantId: string | null; organizationId: string | null }\n | { decrypt: false; reason: 'tenant-mismatch'; rowTenantId: string; callerTenantId: string }\n\nexport type DecryptScopeInput = {\n rowTenantId?: string | null\n rowOrganizationId?: string | null\n callerTenantId?: string | null\n callerOrganizationId?: string | null\n}\n\n/**\n * Decides whether a row may be decrypted, and with which scope.\n *\n * Decryption keys off the row's own `tenant_id` by design: a genuinely cross-tenant read\n * passes no caller tenant, so the row is the only available key id. That precedence becomes\n * dangerous when the caller DID assert a tenant and the row contradicts it \u2014 the engine would\n * fetch the foreign tenant's DEK and hand back its plaintext, amplifying a scoping bug instead\n * of containing it. In that state the row is refused: callers return it untouched (ciphertext).\n *\n * Organization is deliberately NOT compared. Organizations inside a tenant share the tenant DEK,\n * so an org mismatch does not change which key is used, and comparing it would break legitimate\n * multi-org reads for no key-scope benefit.\n *\n * That same argument does NOT exempt a `keyScope: 'system'` map, even though its key id is\n * `system:<entityId>` and ignores the tenant entirely, so a mismatch there changes nothing about\n * which key would be used. This helper is intentionally id-only \u2014 it never sees the map \u2014 so it\n * refuses those rows too. That is conservative rather than correct-by-construction, and it is\n * unreachable today: the only shipped system-scoped map is `onboarding:onboarding_request`, whose\n * readers pass either no scope or the same tenant they filter on. A future system-scoped map read\n * across tenants would need the decision moved to a caller that can see `keyScope`.\n *\n * The refusal branch engages only when BOTH ids are present. Two shapes therefore degrade instead\n * of refusing, and both are safe:\n * - No row tenant (`rowTenantId` null) \u2014 the caller's own tenant is used, which is the strongest\n * form of the binding: a foreign row can only ever be attempted with the caller's DEK. Both query\n * engines force `tenant_id` into the decrypt-decision projection so a narrow `fields` list cannot\n * silently land here, but a table without a `tenant_id` column genuinely has no row tenant.\n * - No caller tenant (`callerTenantId` null) \u2014 there is no entitlement to bind against, so the row's\n * own tenant wins exactly as it did before this guard existed. The ORM path reaches this whenever\n * a `find*WithDecryption` call passes no scope.\n */\nexport function resolveDecryptScope(input: DecryptScopeInput): DecryptScopeDecision {\n const rowTenantId = input.rowTenantId ?? null\n const callerTenantId = input.callerTenantId ?? null\n if (rowTenantId && callerTenantId && rowTenantId !== callerTenantId) {\n return { decrypt: false, reason: 'tenant-mismatch', rowTenantId, callerTenantId }\n }\n return {\n decrypt: true,\n tenantId: rowTenantId ?? callerTenantId,\n organizationId: input.rowOrganizationId ?? input.callerOrganizationId ?? null,\n }\n}\n\nexport type DecryptEnabledInput = {\n decryptEncryptedFields?: boolean\n}\n\n/**\n * Resolves whether a query decrypts its encrypted fields at all.\n *\n * A pure opt-in to declining decryption: unset means today's behaviour, and only an explicit\n * `false` turns decryption off. It is deliberately NOT defaulted off for\n * `omitAutomaticTenantOrgScope` queries \u2014 both engines require `opts.tenantId` on every query,\n * so `resolveDecryptScope` always has a caller tenant to bind against and already refuses any\n * foreign-tenant row on those reads. Flipping the default there would only strip plaintext from\n * legitimately global rows (`tenant_id IS NULL`), which the guard correctly allows.\n *\n * Declining also disables the plaintext-sort path, so a sort on an encrypted field degrades to\n * SQL ordering over ciphertext. The engines warn once when that happens.\n */\nexport function resolveDecryptEnabled(opts: DecryptEnabledInput): boolean {\n return opts.decryptEncryptedFields !== false\n}\n\nconst MAX_SAMPLED_TENANT_IDS = 3\n\nexport const DECRYPT_REFUSAL_LOG_MESSAGE =\n 'Skipped decryption for rows whose tenant does not match the query tenant'\n\nexport const DECLINED_ENCRYPTED_SORT_LOG_MESSAGE =\n 'Sorting on an encrypted field while decryption is declined; rows are ordered by ciphertext'\n\n/**\n * Collects refused rows during one query execution so the caller can emit a single aggregated\n * warning instead of one line per row \u2014 a mis-scoped query can refuse a whole page.\n *\n * Refusals are deduplicated by row id because the plaintext-sort path decrypts an overlapping row\n * set twice (a candidate scan, then the page rows); counting both passes would overstate the\n * refusal count by up to a page. Rows recorded without an id cannot be deduplicated and are\n * counted individually.\n */\nexport class DecryptRefusalTally {\n private readonly refusedRowIds = new Set<string>()\n private unidentifiedCount = 0\n private readonly rowTenantIds = new Set<string>()\n private callerTenantId: string | null = null\n\n record(decision: Extract<DecryptScopeDecision, { decrypt: false }>, rowId?: string | null): void {\n if (rowId === undefined || rowId === null || rowId === '') this.unidentifiedCount += 1\n else this.refusedRowIds.add(String(rowId))\n this.callerTenantId = decision.callerTenantId\n if (this.rowTenantIds.size < MAX_SAMPLED_TENANT_IDS) this.rowTenantIds.add(decision.rowTenantId)\n }\n\n get refused(): number {\n return this.refusedRowIds.size + this.unidentifiedCount\n }\n\n toLogContext(entity: string): Record<string, unknown> {\n return {\n entity,\n refusedRows: this.refused,\n callerTenantId: this.callerTenantId,\n rowTenantIds: Array.from(this.rowTenantIds),\n }\n }\n}\n"],
5
+ "mappings": "AA0CO,SAAS,oBAAoB,OAAgD;AAClF,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,iBAAiB,MAAM,kBAAkB;AAC/C,MAAI,eAAe,kBAAkB,gBAAgB,gBAAgB;AACnE,WAAO,EAAE,SAAS,OAAO,QAAQ,mBAAmB,aAAa,eAAe;AAAA,EAClF;AACA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,UAAU,eAAe;AAAA,IACzB,gBAAgB,MAAM,qBAAqB,MAAM,wBAAwB;AAAA,EAC3E;AACF;AAmBO,SAAS,sBAAsB,MAAoC;AACxE,SAAO,KAAK,2BAA2B;AACzC;AAEA,MAAM,yBAAyB;AAExB,MAAM,8BACX;AAEK,MAAM,sCACX;AAWK,MAAM,oBAAoB;AAAA,EAA1B;AACL,SAAiB,gBAAgB,oBAAI,IAAY;AACjD,SAAQ,oBAAoB;AAC5B,SAAiB,eAAe,oBAAI,IAAY;AAChD,SAAQ,iBAAgC;AAAA;AAAA,EAExC,OAAO,UAA6D,OAA6B;AAC/F,QAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,GAAI,MAAK,qBAAqB;AAAA,QAChF,MAAK,cAAc,IAAI,OAAO,KAAK,CAAC;AACzC,SAAK,iBAAiB,SAAS;AAC/B,QAAI,KAAK,aAAa,OAAO,uBAAwB,MAAK,aAAa,IAAI,SAAS,WAAW;AAAA,EACjG;AAAA,EAEA,IAAI,UAAkB;AACpB,WAAO,KAAK,cAAc,OAAO,KAAK;AAAA,EACxC;AAAA,EAEA,aAAa,QAAyC;AACpD,WAAO;AAAA,MACL;AAAA,MACA,aAAa,KAAK;AAAA,MAClB,gBAAgB,KAAK;AAAA,MACrB,cAAc,MAAM,KAAK,KAAK,YAAY;AAAA,IAC5C;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -1,5 +1,5 @@
1
1
  import { warnOnEncryptedLikeFilter } from "./likeFilterWarning.js";
2
- import { decryptEntitiesWithFallbackScope } from "./subscriber.js";
2
+ import { decryptEntitiesWithFallbackScope, runWithCallerDecryptScope } from "./subscriber.js";
3
3
  async function findWithDecryption(em, entityName, where, options, scope) {
4
4
  await warnOnEncryptedLikeFilter({
5
5
  em,
@@ -8,13 +8,19 @@ async function findWithDecryption(em, entityName, where, options, scope) {
8
8
  tenantId: scope?.tenantId,
9
9
  encryptionService: scope?.encryptionService
10
10
  });
11
- const records = await em.find(entityName, where, options);
11
+ const refusalTallies = /* @__PURE__ */ new Map();
12
+ const records = await runWithCallerDecryptScope(
13
+ scope ?? {},
14
+ refusalTallies,
15
+ () => em.find(entityName, where, options)
16
+ );
12
17
  if (!Array.isArray(records) || records.length === 0) return records ?? [];
13
18
  await decryptEntitiesWithFallbackScope(records, {
14
19
  em,
15
20
  tenantId: scope?.tenantId ?? null,
16
21
  organizationId: scope?.organizationId ?? null,
17
- encryptionService: scope?.encryptionService ?? null
22
+ encryptionService: scope?.encryptionService ?? null,
23
+ refusalTallies
18
24
  });
19
25
  return records;
20
26
  }
@@ -26,13 +32,19 @@ async function findOneWithDecryption(em, entityName, where, options, scope) {
26
32
  tenantId: scope?.tenantId,
27
33
  encryptionService: scope?.encryptionService
28
34
  });
29
- const record = await em.findOne(entityName, where, options);
35
+ const refusalTallies = /* @__PURE__ */ new Map();
36
+ const record = await runWithCallerDecryptScope(
37
+ scope ?? {},
38
+ refusalTallies,
39
+ () => em.findOne(entityName, where, options)
40
+ );
30
41
  if (!record) return record;
31
42
  await decryptEntitiesWithFallbackScope(record, {
32
43
  em,
33
44
  tenantId: scope?.tenantId ?? null,
34
45
  organizationId: scope?.organizationId ?? null,
35
- encryptionService: scope?.encryptionService ?? null
46
+ encryptionService: scope?.encryptionService ?? null,
47
+ refusalTallies
36
48
  });
37
49
  return record;
38
50
  }
@@ -44,10 +56,11 @@ async function findAndCountWithDecryption(em, entityName, where, options, scope)
44
56
  tenantId: scope?.tenantId,
45
57
  encryptionService: scope?.encryptionService
46
58
  });
47
- const [recordsRaw, count] = await em.findAndCount(
48
- entityName,
49
- where,
50
- options
59
+ const refusalTallies = /* @__PURE__ */ new Map();
60
+ const [recordsRaw, count] = await runWithCallerDecryptScope(
61
+ scope ?? {},
62
+ refusalTallies,
63
+ () => em.findAndCount(entityName, where, options)
51
64
  );
52
65
  const records = Array.isArray(recordsRaw) ? recordsRaw : [];
53
66
  if (!records.length) return [records, count];
@@ -55,7 +68,8 @@ async function findAndCountWithDecryption(em, entityName, where, options, scope)
55
68
  em,
56
69
  tenantId: scope?.tenantId ?? null,
57
70
  organizationId: scope?.organizationId ?? null,
58
- encryptionService: scope?.encryptionService ?? null
71
+ encryptionService: scope?.encryptionService ?? null,
72
+ refusalTallies
59
73
  });
60
74
  return [records, count];
61
75
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/encryption/find.ts"],
4
- "sourcesContent": ["import type {\n EntityManager,\n EntityName,\n FilterQuery,\n FindOneOptions,\n FindOptions,\n} from '@mikro-orm/postgresql'\nimport { warnOnEncryptedLikeFilter } from './likeFilterWarning'\nimport { decryptEntitiesWithFallbackScope } from './subscriber'\nimport type { TenantDataEncryptionService } from './tenantDataEncryptionService'\n\nexport type DecryptionScope = {\n tenantId?: string | null\n organizationId?: string | null\n encryptionService?: TenantDataEncryptionService | null\n}\n\ntype AnyFindOptions<Entity extends object, Hint extends string = any> = FindOptions<Entity, Hint, any, any>\ntype AnyFindOneOptions<Entity extends object, Hint extends string = any> = FindOneOptions<Entity, Hint, any, any>\n\nexport async function findWithDecryption<Entity extends object, Hint extends string = any>(\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<Entity[]> {\n await warnOnEncryptedLikeFilter({\n em,\n entityName,\n where,\n tenantId: scope?.tenantId,\n encryptionService: scope?.encryptionService,\n })\n const records = (await em.find<Entity, Hint, any, any>(entityName as any, where as any, options as any)) as any as\n | Entity[]\n | undefined\n if (!Array.isArray(records) || records.length === 0) return records ?? []\n await decryptEntitiesWithFallbackScope(records, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n })\n return records\n}\n\nexport async function findOneWithDecryption<Entity extends object, Hint extends string = any>( // NOSONAR \u2014 returns null or decrypted entity; same variable name, different values\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOneOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<Entity | null> {\n await warnOnEncryptedLikeFilter({\n em,\n entityName,\n where,\n tenantId: scope?.tenantId,\n encryptionService: scope?.encryptionService,\n })\n const record = (await em.findOne<Entity, Hint, any, any>(entityName as any, where as any, options as any)) as any as\n | Entity\n | null\n if (!record) return record\n await decryptEntitiesWithFallbackScope(record, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n })\n return record\n}\n\nexport async function findAndCountWithDecryption<Entity extends object, Hint extends string = any>(\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<[Entity[], number]> {\n await warnOnEncryptedLikeFilter({\n em,\n entityName,\n where,\n tenantId: scope?.tenantId,\n encryptionService: scope?.encryptionService,\n })\n const [recordsRaw, count] = await em.findAndCount<Entity, Hint, any, any>(\n entityName as any,\n where as any,\n options as any,\n ) as any as [Entity[] | undefined, number]\n const records = Array.isArray(recordsRaw) ? recordsRaw : []\n if (!records.length) return [records, count]\n await decryptEntitiesWithFallbackScope(records, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n })\n return [records, count]\n}\n"],
5
- "mappings": "AAOA,SAAS,iCAAiC;AAC1C,SAAS,wCAAwC;AAYjD,eAAsB,mBACpB,IACA,YACA,OACA,SACA,OACmB;AACnB,QAAM,0BAA0B;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,mBAAmB,OAAO;AAAA,EAC5B,CAAC;AACD,QAAM,UAAW,MAAM,GAAG,KAA6B,YAAmB,OAAc,OAAc;AAGtG,MAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,EAAG,QAAO,WAAW,CAAC;AACxE,QAAM,iCAAiC,SAAS;AAAA,IAC9C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,EACjD,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,sBACpB,IACA,YACA,OACA,SACA,OACwB;AACxB,QAAM,0BAA0B;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,mBAAmB,OAAO;AAAA,EAC5B,CAAC;AACD,QAAM,SAAU,MAAM,GAAG,QAAgC,YAAmB,OAAc,OAAc;AAGxG,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,iCAAiC,QAAQ;AAAA,IAC7C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,EACjD,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,2BACpB,IACA,YACA,OACA,SACA,OAC6B;AAC7B,QAAM,0BAA0B;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,mBAAmB,OAAO;AAAA,EAC5B,CAAC;AACD,QAAM,CAAC,YAAY,KAAK,IAAI,MAAM,GAAG;AAAA,IACnC;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,UAAU,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC;AAC1D,MAAI,CAAC,QAAQ,OAAQ,QAAO,CAAC,SAAS,KAAK;AAC3C,QAAM,iCAAiC,SAAS;AAAA,IAC9C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,EACjD,CAAC;AACD,SAAO,CAAC,SAAS,KAAK;AACxB;",
4
+ "sourcesContent": ["import type {\n EntityManager,\n EntityName,\n FilterQuery,\n FindOneOptions,\n FindOptions,\n} from '@mikro-orm/postgresql'\nimport { warnOnEncryptedLikeFilter } from './likeFilterWarning'\nimport { decryptEntitiesWithFallbackScope, runWithCallerDecryptScope } from './subscriber'\nimport { DecryptRefusalTally } from './decryptScope'\nimport type { TenantDataEncryptionService } from './tenantDataEncryptionService'\n\nexport type DecryptionScope = {\n tenantId?: string | null\n organizationId?: string | null\n encryptionService?: TenantDataEncryptionService | null\n}\n\ntype AnyFindOptions<Entity extends object, Hint extends string = any> = FindOptions<Entity, Hint, any, any>\ntype AnyFindOneOptions<Entity extends object, Hint extends string = any> = FindOneOptions<Entity, Hint, any, any>\n\nexport async function findWithDecryption<Entity extends object, Hint extends string = any>(\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<Entity[]> {\n await warnOnEncryptedLikeFilter({\n em,\n entityName,\n where,\n tenantId: scope?.tenantId,\n encryptionService: scope?.encryptionService,\n })\n const refusalTallies = new Map<string, DecryptRefusalTally>()\n const records = (await runWithCallerDecryptScope(scope ?? {}, refusalTallies, () =>\n em.find<Entity, Hint, any, any>(entityName as any, where as any, options as any),\n )) as any as Entity[] | undefined\n if (!Array.isArray(records) || records.length === 0) return records ?? []\n await decryptEntitiesWithFallbackScope(records, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n refusalTallies,\n })\n return records\n}\n\nexport async function findOneWithDecryption<Entity extends object, Hint extends string = any>( // NOSONAR \u2014 returns null or decrypted entity; same variable name, different values\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOneOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<Entity | null> {\n await warnOnEncryptedLikeFilter({\n em,\n entityName,\n where,\n tenantId: scope?.tenantId,\n encryptionService: scope?.encryptionService,\n })\n const refusalTallies = new Map<string, DecryptRefusalTally>()\n const record = (await runWithCallerDecryptScope(scope ?? {}, refusalTallies, () =>\n em.findOne<Entity, Hint, any, any>(entityName as any, where as any, options as any),\n )) as any as Entity | null\n if (!record) return record\n await decryptEntitiesWithFallbackScope(record, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n refusalTallies,\n })\n return record\n}\n\nexport async function findAndCountWithDecryption<Entity extends object, Hint extends string = any>(\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<[Entity[], number]> {\n await warnOnEncryptedLikeFilter({\n em,\n entityName,\n where,\n tenantId: scope?.tenantId,\n encryptionService: scope?.encryptionService,\n })\n const refusalTallies = new Map<string, DecryptRefusalTally>()\n const [recordsRaw, count] = (await runWithCallerDecryptScope(scope ?? {}, refusalTallies, () =>\n em.findAndCount<Entity, Hint, any, any>(entityName as any, where as any, options as any),\n )) as any as [Entity[] | undefined, number]\n const records = Array.isArray(recordsRaw) ? recordsRaw : []\n if (!records.length) return [records, count]\n await decryptEntitiesWithFallbackScope(records, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n refusalTallies,\n })\n return [records, count]\n}\n"],
5
+ "mappings": "AAOA,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC,iCAAiC;AAa5E,eAAsB,mBACpB,IACA,YACA,OACA,SACA,OACmB;AACnB,QAAM,0BAA0B;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,mBAAmB,OAAO;AAAA,EAC5B,CAAC;AACD,QAAM,iBAAiB,oBAAI,IAAiC;AAC5D,QAAM,UAAW,MAAM;AAAA,IAA0B,SAAS,CAAC;AAAA,IAAG;AAAA,IAAgB,MAC5E,GAAG,KAA6B,YAAmB,OAAc,OAAc;AAAA,EACjF;AACA,MAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,EAAG,QAAO,WAAW,CAAC;AACxE,QAAM,iCAAiC,SAAS;AAAA,IAC9C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,IAC/C;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,sBACpB,IACA,YACA,OACA,SACA,OACwB;AACxB,QAAM,0BAA0B;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,mBAAmB,OAAO;AAAA,EAC5B,CAAC;AACD,QAAM,iBAAiB,oBAAI,IAAiC;AAC5D,QAAM,SAAU,MAAM;AAAA,IAA0B,SAAS,CAAC;AAAA,IAAG;AAAA,IAAgB,MAC3E,GAAG,QAAgC,YAAmB,OAAc,OAAc;AAAA,EACpF;AACA,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,iCAAiC,QAAQ;AAAA,IAC7C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,IAC/C;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,2BACpB,IACA,YACA,OACA,SACA,OAC6B;AAC7B,QAAM,0BAA0B;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,mBAAmB,OAAO;AAAA,EAC5B,CAAC;AACD,QAAM,iBAAiB,oBAAI,IAAiC;AAC5D,QAAM,CAAC,YAAY,KAAK,IAAK,MAAM;AAAA,IAA0B,SAAS,CAAC;AAAA,IAAG;AAAA,IAAgB,MACxF,GAAG,aAAqC,YAAmB,OAAc,OAAc;AAAA,EACzF;AACA,QAAM,UAAU,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC;AAC1D,MAAI,CAAC,QAAQ,OAAQ,QAAO,CAAC,SAAS,KAAK;AAC3C,QAAM,iCAAiC,SAAS;AAAA,IAC9C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,IAC/C;AAAA,EACF,CAAC;AACD,SAAO,CAAC,SAAS,KAAK;AACxB;",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { DriverException, ReferenceKind } from "@mikro-orm/core";
2
3
  import { resolveEntityIdFromMetadata } from "./entityIds.js";
3
4
  import { parseDecryptedFieldValue } from "./tenantDataEncryptionService.js";
@@ -6,8 +7,24 @@ import { isTenantDataEncryptionEnabled } from "./toggles.js";
6
7
  import { isEncryptionDebugEnabled } from "./toggles.js";
7
8
  import { resolveTenantEncryptionService } from "./customFieldValues.js";
8
9
  import { createLogger } from "../logger/index.js";
10
+ import { DECRYPT_REFUSAL_LOG_MESSAGE, DecryptRefusalTally, resolveDecryptScope } from "./decryptScope.js";
9
11
  import { listEntityMetadataFromRegistry } from "../db/entityMetadata.js";
10
12
  const logger = createLogger("shared").child({ component: "encryption" });
13
+ const callerDecryptContext = new AsyncLocalStorage();
14
+ function toCallerScope(tenantId, organizationId) {
15
+ if (!tenantId && !organizationId) return void 0;
16
+ return { tenantId: tenantId ?? null, organizationId: organizationId ?? null };
17
+ }
18
+ async function runWithCallerDecryptScope(scope, refusalTallies, read) {
19
+ const callerScope = toCallerScope(scope.tenantId, scope.organizationId);
20
+ if (!callerScope) return callerDecryptContext.exit(read);
21
+ return callerDecryptContext.run({ scope: callerScope, refusalTallies }, read);
22
+ }
23
+ function readCallerDecryptContext() {
24
+ const context = callerDecryptContext.getStore();
25
+ if (!context) return {};
26
+ return { fallbackScope: context.scope, refusalTallies: context.refusalTallies };
27
+ }
11
28
  function resolveScope(entity) {
12
29
  const tenantId = entity.tenantId ?? entity.tenant_id ?? entity.tenant?.id ?? null;
13
30
  const organizationId = entity.organizationId ?? entity.organization_id ?? entity.organization?.id ?? null;
@@ -273,10 +290,26 @@ class TenantEncryptionSubscriber {
273
290
  async decryptEntityGraph(target, meta, em, opts = {}) {
274
291
  await this.decrypt(target, meta, em, opts);
275
292
  }
293
+ /**
294
+ * Scope handed to a loaded relation. The caller's tenant binding is inherited only through a parent
295
+ * that carries a tenant of its own (and so already passed the binding): a tenant-less parent such as
296
+ * a join row may legitimately link entities of several tenants, so its relations keep the caller's
297
+ * scope as a key fallback but are not refused against it. Entities hydrated by the same read are
298
+ * still checked against the caller directly in `onLoad`.
299
+ */
300
+ resolveRelationFallback(fallbackScope, tenantId, organizationId) {
301
+ if (tenantId) {
302
+ if (fallbackScope && fallbackScope.bindsTenant !== false) return fallbackScope;
303
+ return { tenantId, organizationId };
304
+ }
305
+ if (fallbackScope) return { ...fallbackScope, bindsTenant: false };
306
+ return { tenantId: null, organizationId };
307
+ }
276
308
  async decrypt(target, meta, em, {
277
309
  syncOriginal = false,
278
310
  seen,
279
- fallbackScope
311
+ fallbackScope,
312
+ refusalTallies
280
313
  } = {}) {
281
314
  const visited = seen ?? /* @__PURE__ */ new WeakSet();
282
315
  if (visited.has(target)) return;
@@ -289,8 +322,27 @@ class TenantEncryptionSubscriber {
289
322
  const entityId = this.resolveEntityId(resolvedMeta);
290
323
  if (!entityId) return;
291
324
  const { tenantId, organizationId } = resolveScope(target);
292
- const scopedTenantId = tenantId ?? fallbackScope?.tenantId ?? null;
293
- const scopedOrgId = organizationId ?? fallbackScope?.organizationId ?? null;
325
+ const decision = resolveDecryptScope({
326
+ rowTenantId: tenantId,
327
+ rowOrganizationId: organizationId,
328
+ callerTenantId: fallbackScope?.bindsTenant === false ? null : fallbackScope?.tenantId ?? null,
329
+ callerOrganizationId: fallbackScope?.organizationId ?? null
330
+ });
331
+ if (!decision.decrypt) {
332
+ const rowId = target.id;
333
+ if (refusalTallies) {
334
+ const tally = refusalTallies.get(entityId) ?? new DecryptRefusalTally();
335
+ refusalTallies.set(entityId, tally);
336
+ tally.record(decision, rowId == null ? null : String(rowId));
337
+ } else {
338
+ const tally = new DecryptRefusalTally();
339
+ tally.record(decision, rowId == null ? null : String(rowId));
340
+ logger.warn(DECRYPT_REFUSAL_LOG_MESSAGE, tally.toLogContext(entityId));
341
+ }
342
+ return;
343
+ }
344
+ const scopedTenantId = decision.tenantId ?? fallbackScope?.tenantId ?? null;
345
+ const scopedOrgId = decision.organizationId;
294
346
  const hadPendingChanges = syncOriginal ? this.hasPendingChanges(target, resolvedMeta, em) : false;
295
347
  const decrypted = await this.service.decryptEntityPayload(entityId, target, scopedTenantId, scopedOrgId, {
296
348
  em
@@ -300,7 +352,7 @@ class TenantEncryptionSubscriber {
300
352
  if (syncOriginal && !hadPendingChanges) {
301
353
  this.syncOriginalEntityData(target, resolvedMeta, em);
302
354
  }
303
- const nextFallback = fallbackScope ?? (tenantId || organizationId ? { tenantId: tenantId ?? null, organizationId: organizationId ?? null } : { tenantId: scopedTenantId, organizationId: scopedOrgId });
355
+ const nextFallback = this.resolveRelationFallback(fallbackScope, tenantId, organizationId);
304
356
  try {
305
357
  const extractEntities = (value) => {
306
358
  if (!value) return [];
@@ -339,7 +391,8 @@ class TenantEncryptionSubscriber {
339
391
  await this.decrypt(nested, nestedMeta, em, {
340
392
  syncOriginal: true,
341
393
  seen: visited,
342
- fallbackScope: nextFallback
394
+ fallbackScope: nextFallback,
395
+ refusalTallies
343
396
  });
344
397
  }
345
398
  continue;
@@ -352,7 +405,8 @@ class TenantEncryptionSubscriber {
352
405
  await this.decrypt(item, nestedMeta, em, {
353
406
  syncOriginal: true,
354
407
  seen: visited,
355
- fallbackScope: nextFallback
408
+ fallbackScope: nextFallback,
409
+ refusalTallies
356
410
  });
357
411
  }
358
412
  }
@@ -382,7 +436,7 @@ class TenantEncryptionSubscriber {
382
436
  await this.decrypt(args.entity, args.meta, args.em, { syncOriginal: true });
383
437
  }
384
438
  async onLoad(args) {
385
- await this.decrypt(args.entity, args.meta, args.em, { syncOriginal: true });
439
+ await this.decrypt(args.entity, args.meta, args.em, { syncOriginal: true, ...readCallerDecryptContext() });
386
440
  }
387
441
  afterFlush(args) {
388
442
  forgetEncryptionPolicyMemo(args.em);
@@ -390,7 +444,7 @@ class TenantEncryptionSubscriber {
390
444
  async afterFind(args) {
391
445
  const entities = Array.isArray(args.entities) ? args.entities : [];
392
446
  for (const entity of entities) {
393
- await this.decrypt(entity, args.meta, args.em, { syncOriginal: true });
447
+ await this.decrypt(entity, args.meta, args.em, { syncOriginal: true, ...readCallerDecryptContext() });
394
448
  }
395
449
  }
396
450
  }
@@ -405,7 +459,8 @@ async function decryptEntitiesWithFallbackScope(targets, {
405
459
  em,
406
460
  tenantId,
407
461
  organizationId,
408
- encryptionService
462
+ encryptionService,
463
+ refusalTallies = /* @__PURE__ */ new Map()
409
464
  }) {
410
465
  if (!isTenantDataEncryptionEnabled()) return;
411
466
  const list = Array.isArray(targets) ? targets : [targets];
@@ -413,23 +468,26 @@ async function decryptEntitiesWithFallbackScope(targets, {
413
468
  const service = encryptionService ?? resolveTenantEncryptionService(em);
414
469
  if (!service || !service.isEnabled()) return;
415
470
  const subscriber = getSubscriberForService(service);
416
- const fallback = tenantId || organizationId ? {
417
- tenantId: tenantId ?? null,
418
- organizationId: organizationId ?? null
419
- } : void 0;
471
+ const fallback = toCallerScope(tenantId, organizationId);
420
472
  for (const entity of list) {
421
473
  if (!entity || typeof entity !== "object") continue;
422
474
  const meta = entity.__meta ?? entity.__helper?.__meta;
423
475
  await subscriber.decryptEntityGraph(entity, meta, em, {
424
476
  syncOriginal: true,
425
- fallbackScope: fallback
477
+ fallbackScope: fallback,
478
+ refusalTallies
426
479
  });
427
480
  }
481
+ for (const [entityId, tally] of refusalTallies) {
482
+ if (tally.refused === 0) continue;
483
+ logger.warn(DECRYPT_REFUSAL_LOG_MESSAGE, tally.toLogContext(entityId));
484
+ }
428
485
  }
429
486
  export {
430
487
  TenantEncryptionSubscriber,
431
488
  decryptEntitiesWithFallbackScope,
432
489
  registerTenantEncryptionSubscriber,
433
- resetEncryptionPausedWarnThrottle
490
+ resetEncryptionPausedWarnThrottle,
491
+ runWithCallerDecryptScope
434
492
  };
435
493
  //# sourceMappingURL=subscriber.js.map
@@ -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, FlushEventArgs } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { DriverException, ReferenceKind } from '@mikro-orm/core'\nimport { resolveEntityIdFromMetadata } from './entityIds'\nimport { TenantDataEncryptionService, parseDecryptedFieldValue } from './tenantDataEncryptionService'\nimport { forgetEncryptionPolicyMemo } from './policyMemo'\nimport { isTenantDataEncryptionEnabled } from './toggles'\nimport { isEncryptionDebugEnabled } from './toggles'\nimport { resolveTenantEncryptionService } from './customFieldValues'\nimport { createLogger } from '../logger'\nimport { listEntityMetadataFromRegistry } from '../db/entityMetadata'\n\nconst logger = createLogger('shared').child({ component: 'encryption' })\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 logger.debug(event, payload)\n } catch {\n // ignore\n }\n}\n\nfunction isJsonColumnProperty(prop: unknown): boolean {\n if (!prop || typeof prop !== 'object') return false\n const candidate = prop as Record<string, unknown>\n const type = typeof candidate.type === 'string' ? candidate.type.toLowerCase() : ''\n if (type === 'json' || type === 'jsonb') return true\n const columnTypes = Array.isArray(candidate.columnTypes) ? candidate.columnTypes : []\n if (columnTypes.some((value) => typeof value === 'string' && value.toLowerCase().includes('json'))) return true\n const customTypeName = (candidate.customType as { constructor?: { name?: string } } | undefined)?.constructor?.name\n return typeof customTypeName === 'string' && customTypeName.toLowerCase().includes('json')\n}\n\n// Encryption is configured on but the service reports disabled \u2014 the KMS is\n// unreachable, so this write lands as plaintext. Throttle the warning so a long\n// outage does not flood the log while still leaving an ongoing signal that the\n// fail-open window is open (#5948).\nconst ENCRYPTION_PAUSED_WARN_INTERVAL_MS = 60_000\nlet lastEncryptionPausedWarnAt = 0\n\nfunction warnEncryptionPaused(entity: string | undefined): void {\n const now = Date.now()\n if (lastEncryptionPausedWarnAt && now - lastEncryptionPausedWarnAt < ENCRYPTION_PAUSED_WARN_INTERVAL_MS) return\n lastEncryptionPausedWarnAt = now\n try {\n logger.warn(\n 'Tenant data encryption is enabled but the KMS is unavailable - entity writes persist as plaintext until it recovers',\n { entity },\n )\n } catch {\n // ignore\n }\n}\n\nexport function resetEncryptionPausedWarnThrottle(): void {\n lastEncryptionPausedWarnAt = 0\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\nfunction isDatabaseError(err: unknown): boolean {\n if (err instanceof DriverException) return true\n if (!err || typeof err !== 'object') return false\n const candidate = err as { code?: unknown; severity?: unknown; sqlState?: unknown }\n if (typeof candidate.sqlState === 'string') return true\n return typeof candidate.code === 'string' && typeof candidate.severity === 'string'\n}\n\nfunction isInTransaction(em: unknown): boolean {\n if (!em || typeof em !== 'object') return false\n const candidate = em as { isInTransaction?: () => boolean; getTransactionContext?: () => unknown }\n try {\n if (typeof candidate.isInTransaction === 'function') return candidate.isInTransaction()\n if (typeof candidate.getTransactionContext === 'function') return candidate.getTransactionContext() != null\n } catch {\n return false\n }\n return false\n}\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 try {\n const entries = listEntityMetadataFromRegistry(registry)\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 restoreDecryptedJsonColumns(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n ) {\n const properties = meta?.properties\n if (!properties || typeof properties !== 'object') return\n for (const [propertyName, prop] of Object.entries(properties)) {\n if (!isJsonColumnProperty(prop)) continue\n const value = target[propertyName]\n if (typeof value !== 'string') continue\n const parsed = parseDecryptedFieldValue(value)\n if (parsed !== value) target[propertyName] = parsed\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 const encryptionConfigured = isTenantDataEncryptionEnabled()\n if (!encryptionConfigured || !this.service.isEnabled()) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'disabled', entity: meta?.className || meta?.name })\n if (encryptionConfigured) warnEncryptionPaused(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 // Hand the flushing EntityManager to the policy read so it runs on this\n // transaction's own connection instead of acquiring a second pooled one\n // (an uncached lookup from inside an open write transaction deadlocks the\n // pool once every slot is held by such a transaction).\n const encrypted = await this.service.encryptEntityPayload(entityId, target, tenantId, organizationId, {\n em: em as EntityManager | undefined,\n })\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 // 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 em: em as EntityManager | undefined,\n })\n Object.assign(target, decrypted)\n this.restoreDecryptedJsonColumns(target, resolvedMeta)\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 Collection wrapper \u2014 MUST be checked before the Reference branch: both wrappers\n // expose isInitialized(), but only a Collection exposes getItems(). Matching the Reference\n // branch first returned the Collection wrapper itself instead of its items, so collection\n // relations were never deep-decrypted and leaked ciphertext (issue #2744).\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 // 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 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 // A failed statement aborts the surrounding Postgres transaction; swallowing it here would\n // only move the failure to the caller's next statement (\"current transaction is aborted\").\n if (isDatabaseError(err) && isInTransaction(em)) throw 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 afterFlush(args: FlushEventArgs) {\n forgetEncryptionPolicyMemo(args.em as unknown as EntityManager)\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 { AsyncLocalStorage } from 'node:async_hooks'\nimport type { EntityMetadata, EventArgs, EventSubscriber, FlushEventArgs } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { DriverException, ReferenceKind } from '@mikro-orm/core'\nimport { resolveEntityIdFromMetadata } from './entityIds'\nimport { TenantDataEncryptionService, parseDecryptedFieldValue } from './tenantDataEncryptionService'\nimport { forgetEncryptionPolicyMemo } from './policyMemo'\nimport { isTenantDataEncryptionEnabled } from './toggles'\nimport { isEncryptionDebugEnabled } from './toggles'\nimport { resolveTenantEncryptionService } from './customFieldValues'\nimport { createLogger } from '../logger'\nimport { DECRYPT_REFUSAL_LOG_MESSAGE, DecryptRefusalTally, resolveDecryptScope } from './decryptScope'\nimport { listEntityMetadataFromRegistry } from '../db/entityMetadata'\n\nconst logger = createLogger('shared').child({ component: 'encryption' })\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; bindsTenant?: boolean }\n\ntype CallerDecryptContext = { scope: Scope; refusalTallies: Map<string, DecryptRefusalTally> }\n\nconst callerDecryptContext = new AsyncLocalStorage<CallerDecryptContext>()\n\nfunction toCallerScope(tenantId?: string | null, organizationId?: string | null): Scope | undefined {\n if (!tenantId && !organizationId) return undefined\n return { tenantId: tenantId ?? null, organizationId: organizationId ?? null }\n}\n\n/**\n * Runs an ORM read so that the `onLoad` / `afterFind` hooks it triggers decrypt under the caller's\n * scope instead of the loaded row's own tenant. Without it, hydration would decrypt a foreign-tenant\n * entity with that tenant's DEK before `find*WithDecryption` gets to apply the tenant binding.\n * Refusals raised during hydration are collected in `refusalTallies` so the caller reports them once.\n */\nexport async function runWithCallerDecryptScope<T>(\n scope: { tenantId?: string | null; organizationId?: string | null },\n refusalTallies: Map<string, DecryptRefusalTally>,\n read: () => Promise<T>,\n): Promise<T> {\n const callerScope = toCallerScope(scope.tenantId, scope.organizationId)\n if (!callerScope) return callerDecryptContext.exit(read)\n return callerDecryptContext.run({ scope: callerScope, refusalTallies }, read)\n}\n\nfunction readCallerDecryptContext(): { fallbackScope?: Scope; refusalTallies?: Map<string, DecryptRefusalTally> } {\n const context = callerDecryptContext.getStore()\n if (!context) return {}\n return { fallbackScope: context.scope, refusalTallies: context.refusalTallies }\n}\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 logger.debug(event, payload)\n } catch {\n // ignore\n }\n}\n\nfunction isJsonColumnProperty(prop: unknown): boolean {\n if (!prop || typeof prop !== 'object') return false\n const candidate = prop as Record<string, unknown>\n const type = typeof candidate.type === 'string' ? candidate.type.toLowerCase() : ''\n if (type === 'json' || type === 'jsonb') return true\n const columnTypes = Array.isArray(candidate.columnTypes) ? candidate.columnTypes : []\n if (columnTypes.some((value) => typeof value === 'string' && value.toLowerCase().includes('json'))) return true\n const customTypeName = (candidate.customType as { constructor?: { name?: string } } | undefined)?.constructor?.name\n return typeof customTypeName === 'string' && customTypeName.toLowerCase().includes('json')\n}\n\n// Encryption is configured on but the service reports disabled \u2014 the KMS is\n// unreachable, so this write lands as plaintext. Throttle the warning so a long\n// outage does not flood the log while still leaving an ongoing signal that the\n// fail-open window is open (#5948).\nconst ENCRYPTION_PAUSED_WARN_INTERVAL_MS = 60_000\nlet lastEncryptionPausedWarnAt = 0\n\nfunction warnEncryptionPaused(entity: string | undefined): void {\n const now = Date.now()\n if (lastEncryptionPausedWarnAt && now - lastEncryptionPausedWarnAt < ENCRYPTION_PAUSED_WARN_INTERVAL_MS) return\n lastEncryptionPausedWarnAt = now\n try {\n logger.warn(\n 'Tenant data encryption is enabled but the KMS is unavailable - entity writes persist as plaintext until it recovers',\n { entity },\n )\n } catch {\n // ignore\n }\n}\n\nexport function resetEncryptionPausedWarnThrottle(): void {\n lastEncryptionPausedWarnAt = 0\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\nfunction isDatabaseError(err: unknown): boolean {\n if (err instanceof DriverException) return true\n if (!err || typeof err !== 'object') return false\n const candidate = err as { code?: unknown; severity?: unknown; sqlState?: unknown }\n if (typeof candidate.sqlState === 'string') return true\n return typeof candidate.code === 'string' && typeof candidate.severity === 'string'\n}\n\nfunction isInTransaction(em: unknown): boolean {\n if (!em || typeof em !== 'object') return false\n const candidate = em as { isInTransaction?: () => boolean; getTransactionContext?: () => unknown }\n try {\n if (typeof candidate.isInTransaction === 'function') return candidate.isInTransaction()\n if (typeof candidate.getTransactionContext === 'function') return candidate.getTransactionContext() != null\n } catch {\n return false\n }\n return false\n}\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 try {\n const entries = listEntityMetadataFromRegistry(registry)\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 restoreDecryptedJsonColumns(\n target: Record<string, unknown>,\n meta: EntityMetadata<any> | undefined,\n ) {\n const properties = meta?.properties\n if (!properties || typeof properties !== 'object') return\n for (const [propertyName, prop] of Object.entries(properties)) {\n if (!isJsonColumnProperty(prop)) continue\n const value = target[propertyName]\n if (typeof value !== 'string') continue\n const parsed = parseDecryptedFieldValue(value)\n if (parsed !== value) target[propertyName] = parsed\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 const encryptionConfigured = isTenantDataEncryptionEnabled()\n if (!encryptionConfigured || !this.service.isEnabled()) {\n debug('\u26AA\uFE0F subscriber.skip', { reason: 'disabled', entity: meta?.className || meta?.name })\n if (encryptionConfigured) warnEncryptionPaused(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 // Hand the flushing EntityManager to the policy read so it runs on this\n // transaction's own connection instead of acquiring a second pooled one\n // (an uncached lookup from inside an open write transaction deadlocks the\n // pool once every slot is held by such a transaction).\n const encrypted = await this.service.encryptEntityPayload(entityId, target, tenantId, organizationId, {\n em: em as EntityManager | undefined,\n })\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: {\n syncOriginal?: boolean\n seen?: WeakSet<object>\n fallbackScope?: Scope\n refusalTallies?: Map<string, DecryptRefusalTally>\n } = {},\n ) {\n await this.decrypt(target, meta, em, opts)\n }\n\n /**\n * Scope handed to a loaded relation. The caller's tenant binding is inherited only through a parent\n * that carries a tenant of its own (and so already passed the binding): a tenant-less parent such as\n * a join row may legitimately link entities of several tenants, so its relations keep the caller's\n * scope as a key fallback but are not refused against it. Entities hydrated by the same read are\n * still checked against the caller directly in `onLoad`.\n */\n private resolveRelationFallback(\n fallbackScope: Scope | undefined,\n tenantId: string | null,\n organizationId: string | null,\n ): Scope {\n if (tenantId) {\n if (fallbackScope && fallbackScope.bindsTenant !== false) return fallbackScope\n return { tenantId, organizationId }\n }\n if (fallbackScope) return { ...fallbackScope, bindsTenant: false }\n return { tenantId: null, organizationId }\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 refusalTallies,\n }: {\n syncOriginal?: boolean\n seen?: WeakSet<object>\n fallbackScope?: Scope\n refusalTallies?: Map<string, DecryptRefusalTally>\n } = {},\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 decision = resolveDecryptScope({\n rowTenantId: tenantId,\n rowOrganizationId: organizationId,\n callerTenantId: fallbackScope?.bindsTenant === false ? null : fallbackScope?.tenantId ?? null,\n callerOrganizationId: fallbackScope?.organizationId ?? null,\n })\n if (!decision.decrypt) {\n const rowId = (target as { id?: unknown }).id\n if (refusalTallies) {\n const tally = refusalTallies.get(entityId) ?? new DecryptRefusalTally()\n refusalTallies.set(entityId, tally)\n tally.record(decision, rowId == null ? null : String(rowId))\n } else {\n const tally = new DecryptRefusalTally()\n tally.record(decision, rowId == null ? null : String(rowId))\n logger.warn(DECRYPT_REFUSAL_LOG_MESSAGE, tally.toLogContext(entityId))\n }\n return\n }\n const scopedTenantId = decision.tenantId ?? fallbackScope?.tenantId ?? null\n const scopedOrgId = decision.organizationId\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 em: em as EntityManager | undefined,\n })\n Object.assign(target, decrypted)\n this.restoreDecryptedJsonColumns(target, resolvedMeta)\n if (syncOriginal && !hadPendingChanges) {\n this.syncOriginalEntityData(target, resolvedMeta, em as any)\n }\n const nextFallback = this.resolveRelationFallback(fallbackScope, tenantId, organizationId)\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 Collection wrapper \u2014 MUST be checked before the Reference branch: both wrappers\n // expose isInitialized(), but only a Collection exposes getItems(). Matching the Reference\n // branch first returned the Collection wrapper itself instead of its items, so collection\n // relations were never deep-decrypted and leaked ciphertext (issue #2744).\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 // 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 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 refusalTallies,\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 refusalTallies,\n })\n }\n }\n }\n } catch (err) {\n // A failed statement aborts the surrounding Postgres transaction; swallowing it here would\n // only move the failure to the caller's next statement (\"current transaction is aborted\").\n if (isDatabaseError(err) && isInTransaction(em)) throw 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, ...readCallerDecryptContext() })\n }\n\n afterFlush(args: FlushEventArgs) {\n forgetEncryptionPolicyMemo(args.em as unknown as EntityManager)\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, ...readCallerDecryptContext() })\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 refusalTallies = new Map<string, DecryptRefusalTally>(),\n }: {\n em: { getMetadata?: () => any; getComparator?: () => any }\n tenantId?: string | null\n organizationId?: string | null\n encryptionService?: TenantDataEncryptionService | null\n refusalTallies?: Map<string, DecryptRefusalTally>\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 = toCallerScope(tenantId, organizationId)\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 refusalTallies,\n })\n }\n for (const [entityId, tally] of refusalTallies) {\n if (tally.refused === 0) continue\n logger.warn(DECRYPT_REFUSAL_LOG_MESSAGE, tally.toLogContext(entityId))\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -18,15 +18,18 @@ function resolveEncryptedSortMaxRows() {
18
18
  if (!Number.isFinite(parsed) || parsed <= 0) return null;
19
19
  return Math.floor(parsed);
20
20
  }
21
- async function resolveEncryptedSortFields(service, entity, sortFields, tenantId, organizationId) {
22
- if (!service?.getEncryptedFieldNames) return /* @__PURE__ */ new Set();
23
- if (service.isEnabled && !service.isEnabled()) return /* @__PURE__ */ new Set();
24
- const encrypted = await service.getEncryptedFieldNames(entity, tenantId ?? null, organizationId ?? null);
21
+ async function resolveEncryptedFieldNames(service, entity, tenantId, organizationId) {
22
+ if (service?.isEnabled && !service.isEnabled()) return [];
23
+ if (!service?.getEncryptedFieldNames) return null;
24
+ return await service.getEncryptedFieldNames(entity, tenantId ?? null, organizationId ?? null);
25
+ }
26
+ function matchEncryptedSortFields(encryptedFieldNames, sortFields) {
27
+ const result = /* @__PURE__ */ new Set();
28
+ if (!encryptedFieldNames || encryptedFieldNames.length === 0) return result;
25
29
  const encryptedCandidates = /* @__PURE__ */ new Set();
26
- for (const field of encrypted) {
30
+ for (const field of encryptedFieldNames) {
27
31
  for (const candidate of fieldNameCandidates(field)) encryptedCandidates.add(candidate);
28
32
  }
29
- const result = /* @__PURE__ */ new Set();
30
33
  for (const field of sortFields) {
31
34
  if (field.startsWith("cf:")) continue;
32
35
  const matches = fieldNameCandidates(field).some((candidate) => encryptedCandidates.has(candidate));
@@ -34,6 +37,10 @@ async function resolveEncryptedSortFields(service, entity, sortFields, tenantId,
34
37
  }
35
38
  return result;
36
39
  }
40
+ async function resolveEncryptedSortFields(service, entity, sortFields, tenantId, organizationId) {
41
+ const encryptedFieldNames = await resolveEncryptedFieldNames(service, entity, tenantId, organizationId);
42
+ return matchEncryptedSortFields(encryptedFieldNames, sortFields);
43
+ }
37
44
  function readField(row, field) {
38
45
  for (const candidate of fieldNameCandidates(field)) {
39
46
  if (Object.prototype.hasOwnProperty.call(row, candidate)) return row[candidate];
@@ -68,6 +75,8 @@ function sortRowsInMemory(rows, sorts) {
68
75
  }
69
76
  export {
70
77
  fieldNameCandidates,
78
+ matchEncryptedSortFields,
79
+ resolveEncryptedFieldNames,
71
80
  resolveEncryptedSortFields,
72
81
  resolveEncryptedSortMaxRows,
73
82
  sortRowsInMemory