@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/lib/crud/factory.js +3 -0
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/encryption/decryptScope.js +51 -0
- package/dist/lib/encryption/decryptScope.js.map +7 -0
- package/dist/lib/encryption/find.js +24 -10
- package/dist/lib/encryption/find.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +73 -15
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/query/encrypted-sort.js +15 -6
- package/dist/lib/query/encrypted-sort.js.map +2 -2
- package/dist/lib/query/engine.js +57 -10
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +16 -0
- package/src/lib/crud/factory.ts +13 -0
- package/src/lib/encryption/__tests__/decrypt-scope.test.ts +134 -0
- package/src/lib/encryption/__tests__/subscriber.onload-tenant-binding.test.ts +234 -0
- package/src/lib/encryption/__tests__/subscriber.refusal-aggregation.test.ts +105 -0
- package/src/lib/encryption/__tests__/subscriber.tenant-binding.test.ts +131 -0
- package/src/lib/encryption/decryptScope.ts +119 -0
- package/src/lib/encryption/find.ts +17 -12
- package/src/lib/encryption/subscriber.ts +101 -19
- package/src/lib/query/__tests__/engine-decrypt-refusal-log.test.ts +168 -0
- package/src/lib/query/__tests__/engine.test.ts +237 -0
- package/src/lib/query/encrypted-sort.ts +34 -8
- package/src/lib/query/engine.ts +70 -10
- package/src/lib/query/types.ts +15 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/query/encrypted-sort.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { SortDir, type Sort } from './types'\n\nexport type QueryEncryptionService = {\n getEncryptedFieldNames?: (\n entityId: EntityId,\n tenantId?: string | null,\n organizationId?: string | null,\n ) => Promise<readonly string[]>\n isEnabled?: () => boolean\n}\n\nconst toSnakeCase = (value: string): string =>\n value.replace(/([A-Z])/g, '_$1').replace(/__/g, '_').toLowerCase()\n\nconst toCamelCase = (value: string): string =>\n value.replace(/_([a-z])/g, (_, c) => c.toUpperCase())\n\nexport function fieldNameCandidates(field: string): string[] {\n const raw = String(field || '').trim()\n if (!raw) return []\n const candidates = [raw, toSnakeCase(raw), toCamelCase(raw)]\n if (raw.startsWith('cf:')) {\n const sanitized = raw.replace(/[^a-zA-Z0-9_]/g, '_')\n // The ORM query engine's plaintext-sort candidate scan projects a `cf:`\n // sort key only under its dedicated `<alias>__sort` projection alias, never\n // the plain jsonb-aggregate alias \u2014 so the in-memory sort needs that exact\n // name to find the value on the phase-1 candidate rows (#5674 review).\n candidates.push(sanitized, `${sanitized}__sort`)\n }\n return Array.from(new Set(candidates))\n}\n\n/**\n * Opt-in cap on how many candidate rows the plaintext-sort path may fetch\n * for sort-column decryption. Unset or invalid input means uncapped \u2014 the\n * default must stay byte-identical to the pre-cap behavior.\n */\nexport function resolveEncryptedSortMaxRows(): number | null {\n const raw = process.env.OM_ENCRYPTED_SORT_MAX_ROWS\n if (raw === undefined || raw === '') return null\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed <= 0) return null\n return Math.floor(parsed)\n}\n\nexport async function
|
|
5
|
-
"mappings": "AACA,SAAS,eAA0B;AAWnC,MAAM,cAAc,CAAC,UACnB,MAAM,QAAQ,YAAY,KAAK,EAAE,QAAQ,OAAO,GAAG,EAAE,YAAY;AAEnE,MAAM,cAAc,CAAC,UACnB,MAAM,QAAQ,aAAa,CAAC,GAAG,MAAM,EAAE,YAAY,CAAC;AAE/C,SAAS,oBAAoB,OAAyB;AAC3D,QAAM,MAAM,OAAO,SAAS,EAAE,EAAE,KAAK;AACrC,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,QAAM,aAAa,CAAC,KAAK,YAAY,GAAG,GAAG,YAAY,GAAG,CAAC;AAC3D,MAAI,IAAI,WAAW,KAAK,GAAG;AACzB,UAAM,YAAY,IAAI,QAAQ,kBAAkB,GAAG;AAKnD,eAAW,KAAK,WAAW,GAAG,SAAS,QAAQ;AAAA,EACjD;AACA,SAAO,MAAM,KAAK,IAAI,IAAI,UAAU,CAAC;AACvC;AAOO,SAAS,8BAA6C;AAC3D,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,QAAQ,UAAa,QAAQ,GAAI,QAAO;AAC5C,QAAM,SAAS,OAAO,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,EAAG,QAAO;AACpD,SAAO,KAAK,MAAM,MAAM;AAC1B;
|
|
4
|
+
"sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { SortDir, type Sort } from './types'\n\nexport type QueryEncryptionService = {\n getEncryptedFieldNames?: (\n entityId: EntityId,\n tenantId?: string | null,\n organizationId?: string | null,\n ) => Promise<readonly string[]>\n isEnabled?: () => boolean\n}\n\nconst toSnakeCase = (value: string): string =>\n value.replace(/([A-Z])/g, '_$1').replace(/__/g, '_').toLowerCase()\n\nconst toCamelCase = (value: string): string =>\n value.replace(/_([a-z])/g, (_, c) => c.toUpperCase())\n\nexport function fieldNameCandidates(field: string): string[] {\n const raw = String(field || '').trim()\n if (!raw) return []\n const candidates = [raw, toSnakeCase(raw), toCamelCase(raw)]\n if (raw.startsWith('cf:')) {\n const sanitized = raw.replace(/[^a-zA-Z0-9_]/g, '_')\n // The ORM query engine's plaintext-sort candidate scan projects a `cf:`\n // sort key only under its dedicated `<alias>__sort` projection alias, never\n // the plain jsonb-aggregate alias \u2014 so the in-memory sort needs that exact\n // name to find the value on the phase-1 candidate rows (#5674 review).\n candidates.push(sanitized, `${sanitized}__sort`)\n }\n return Array.from(new Set(candidates))\n}\n\n/**\n * Opt-in cap on how many candidate rows the plaintext-sort path may fetch\n * for sort-column decryption. Unset or invalid input means uncapped \u2014 the\n * default must stay byte-identical to the pre-cap behavior.\n */\nexport function resolveEncryptedSortMaxRows(): number | null {\n const raw = process.env.OM_ENCRYPTED_SORT_MAX_ROWS\n if (raw === undefined || raw === '') return null\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed <= 0) return null\n return Math.floor(parsed)\n}\n\n/**\n * Reads the entity's encryption map once so a query can answer both \"which sort fields are\n * encrypted\" and \"must the projection carry `tenant_id` into the decrypt decision\" without paying\n * for two lookups.\n *\n * `null` means the map is unavailable \u2014 no service, or one that cannot report field names. A query\n * that reads `null` keeps today's behaviour for sorting (no plaintext-sort path) but must assume a\n * decrypt attempt may still follow. An empty array means the entity genuinely encrypts nothing.\n */\nexport async function resolveEncryptedFieldNames(\n service: QueryEncryptionService | null | undefined,\n entity: EntityId,\n tenantId?: string | null,\n organizationId?: string | null,\n): Promise<readonly string[] | null> {\n if (service?.isEnabled && !service.isEnabled()) return []\n if (!service?.getEncryptedFieldNames) return null\n return await service.getEncryptedFieldNames(entity, tenantId ?? null, organizationId ?? null)\n}\n\nexport function matchEncryptedSortFields(\n encryptedFieldNames: readonly string[] | null,\n sortFields: readonly string[],\n): Set<string> {\n const result = new Set<string>()\n if (!encryptedFieldNames || encryptedFieldNames.length === 0) return result\n const encryptedCandidates = new Set<string>()\n for (const field of encryptedFieldNames) {\n for (const candidate of fieldNameCandidates(field)) encryptedCandidates.add(candidate)\n }\n for (const field of sortFields) {\n if (field.startsWith('cf:')) continue\n const matches = fieldNameCandidates(field).some((candidate) => encryptedCandidates.has(candidate))\n if (matches) result.add(field)\n }\n return result\n}\n\nexport async function resolveEncryptedSortFields(\n service: QueryEncryptionService | null | undefined,\n entity: EntityId,\n sortFields: readonly string[],\n tenantId?: string | null,\n organizationId?: string | null,\n): Promise<Set<string>> {\n const encryptedFieldNames = await resolveEncryptedFieldNames(service, entity, tenantId, organizationId)\n return matchEncryptedSortFields(encryptedFieldNames, sortFields)\n}\n\nfunction readField(row: Record<string, unknown>, field: string): unknown {\n for (const candidate of fieldNameCandidates(field)) {\n if (Object.prototype.hasOwnProperty.call(row, candidate)) return row[candidate]\n }\n return undefined\n}\n\nfunction compareValues(left: unknown, right: unknown): number {\n const leftMissing = left === null || left === undefined\n const rightMissing = right === null || right === undefined\n if (leftMissing && rightMissing) return 0\n if (leftMissing) return 1\n if (rightMissing) return -1\n if (left instanceof Date && right instanceof Date) return left.getTime() - right.getTime()\n if (typeof left === 'number' && typeof right === 'number') return left - right\n if (typeof left === 'boolean' && typeof right === 'boolean') return Number(left) - Number(right)\n return String(left).localeCompare(String(right), undefined, {\n sensitivity: 'base',\n numeric: true,\n })\n}\n\nexport function sortRowsInMemory<T extends Record<string, unknown>>(\n rows: readonly T[],\n sorts: readonly Sort[],\n): T[] {\n return [...rows].sort((left, right) => {\n for (const sort of sorts) {\n const direction = sort.dir === SortDir.Desc ? -1 : 1\n const compared = compareValues(readField(left, sort.field), readField(right, sort.field))\n if (compared !== 0) return compared * direction\n }\n const leftId = readField(left, 'id')\n const rightId = readField(right, 'id')\n return compareValues(leftId, rightId)\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/query/engine.js
CHANGED
|
@@ -21,9 +21,22 @@ import {
|
|
|
21
21
|
resolveCfDefIndexOrgCandidates
|
|
22
22
|
} from "../crud/custom-field-definition-index.js";
|
|
23
23
|
import { warnOnCiphertextLikeFallback } from "./ciphertext-search-warning.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
matchEncryptedSortFields,
|
|
26
|
+
resolveEncryptedFieldNames,
|
|
27
|
+
resolveEncryptedSortFields,
|
|
28
|
+
resolveEncryptedSortMaxRows,
|
|
29
|
+
sortRowsInMemory
|
|
30
|
+
} from "./encrypted-sort.js";
|
|
25
31
|
import { resolveListCountCap } from "./count-cap.js";
|
|
26
32
|
import { mapWithConcurrency } from "./bounded-decrypt.js";
|
|
33
|
+
import {
|
|
34
|
+
DECLINED_ENCRYPTED_SORT_LOG_MESSAGE,
|
|
35
|
+
DECRYPT_REFUSAL_LOG_MESSAGE,
|
|
36
|
+
DecryptRefusalTally,
|
|
37
|
+
resolveDecryptEnabled,
|
|
38
|
+
resolveDecryptScope
|
|
39
|
+
} from "../encryption/decryptScope.js";
|
|
27
40
|
import { parseNumberWithDefault } from "../number.js";
|
|
28
41
|
import { createLogger } from "../logger/index.js";
|
|
29
42
|
import {
|
|
@@ -473,7 +486,8 @@ class BasicQueryEngine {
|
|
|
473
486
|
return next;
|
|
474
487
|
};
|
|
475
488
|
const fallbackOrgId = opts.organizationId ?? (Array.isArray(opts.organizationIds) && opts.organizationIds.length === 1 ? opts.organizationIds[0] : null);
|
|
476
|
-
const
|
|
489
|
+
const decryptEnabled = resolveDecryptEnabled(opts);
|
|
490
|
+
const encryptionService = decryptEnabled ? this.getEncryptionService() : null;
|
|
477
491
|
const resolvedSorts = [];
|
|
478
492
|
for (const s of opts.sort || []) {
|
|
479
493
|
if (s.field.startsWith("cf:")) {
|
|
@@ -483,14 +497,32 @@ class BasicQueryEngine {
|
|
|
483
497
|
if (column) resolvedSorts.push({ ...s, field: column });
|
|
484
498
|
}
|
|
485
499
|
}
|
|
486
|
-
const
|
|
500
|
+
const plainSortFields = resolvedSorts.filter((sort) => !sort.field.startsWith("cf:")).map((sort) => sort.field);
|
|
501
|
+
const encryptedFieldNames = await resolveEncryptedFieldNames(
|
|
487
502
|
encryptionService,
|
|
488
503
|
entity,
|
|
489
|
-
resolvedSorts.filter((sort) => !sort.field.startsWith("cf:")).map((sort) => sort.field),
|
|
490
504
|
opts.tenantId ?? null,
|
|
491
505
|
fallbackOrgId
|
|
492
506
|
);
|
|
507
|
+
const encryptedSortFields = matchEncryptedSortFields(encryptedFieldNames, plainSortFields);
|
|
493
508
|
const requiresPlaintextSort = encryptedSortFields.size > 0;
|
|
509
|
+
if (!decryptEnabled && plainSortFields.length > 0) {
|
|
510
|
+
const declinedSortFields = await resolveEncryptedSortFields(
|
|
511
|
+
this.getEncryptionService(),
|
|
512
|
+
entity,
|
|
513
|
+
plainSortFields,
|
|
514
|
+
opts.tenantId ?? null,
|
|
515
|
+
fallbackOrgId
|
|
516
|
+
);
|
|
517
|
+
if (declinedSortFields.size > 0) {
|
|
518
|
+
logger.warn(DECLINED_ENCRYPTED_SORT_LOG_MESSAGE, {
|
|
519
|
+
entity: String(entity),
|
|
520
|
+
sortFields: Array.from(declinedSortFields)
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const requestedFields = opts.fields && opts.fields.length ? opts.fields.map(String) : null;
|
|
525
|
+
const injectsTenantIdForDecrypt = encryptionService != null && requestedFields != null && !requestedFields.includes("tenant_id") && !requestedFields.includes("tenantId") && (encryptedFieldNames === null || encryptedFieldNames.length > 0) && await this.columnExists(table, "tenant_id");
|
|
494
526
|
const tenantId = opts.tenantId;
|
|
495
527
|
const sanitize = (s) => s.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
496
528
|
const buildQuery = async (projection) => {
|
|
@@ -586,6 +618,7 @@ class BasicQueryEngine {
|
|
|
586
618
|
if (requiresPlaintextSort) {
|
|
587
619
|
for (const field of encryptedSortFields) cols.add(field);
|
|
588
620
|
}
|
|
621
|
+
if (injectsTenantIdForDecrypt) cols.add("tenant_id");
|
|
589
622
|
for (const c of cols) {
|
|
590
623
|
q = q.select(sql.ref(qualify(c)).as(c));
|
|
591
624
|
}
|
|
@@ -979,21 +1012,31 @@ class BasicQueryEngine {
|
|
|
979
1012
|
}
|
|
980
1013
|
const svc = encryptionService;
|
|
981
1014
|
const decryptPayload = svc?.decryptEntityPayload?.bind(svc);
|
|
1015
|
+
const refusalTally = new DecryptRefusalTally();
|
|
982
1016
|
const decryptRow = async (item) => {
|
|
983
1017
|
if (!decryptPayload) return item;
|
|
1018
|
+
const decision = resolveDecryptScope({
|
|
1019
|
+
rowTenantId: item?.tenant_id ?? item?.tenantId ?? null,
|
|
1020
|
+
rowOrganizationId: item?.organization_id ?? item?.organizationId ?? null,
|
|
1021
|
+
callerTenantId: opts.tenantId ?? null,
|
|
1022
|
+
callerOrganizationId: fallbackOrgId ?? null
|
|
1023
|
+
});
|
|
1024
|
+
if (!decision.decrypt) {
|
|
1025
|
+
refusalTally.record(decision, item?.id == null ? null : String(item.id));
|
|
1026
|
+
return item;
|
|
1027
|
+
}
|
|
984
1028
|
try {
|
|
985
|
-
const decrypted = await decryptPayload(
|
|
986
|
-
entity,
|
|
987
|
-
item,
|
|
988
|
-
item?.tenant_id ?? item?.tenantId ?? opts.tenantId ?? null,
|
|
989
|
-
item?.organization_id ?? item?.organizationId ?? fallbackOrgId ?? null
|
|
990
|
-
);
|
|
1029
|
+
const decrypted = await decryptPayload(entity, item, decision.tenantId, decision.organizationId);
|
|
991
1030
|
return { ...item, ...decrypted };
|
|
992
1031
|
} catch (err) {
|
|
993
1032
|
logger.error("Error decrypting entity payload", { err });
|
|
994
1033
|
return item;
|
|
995
1034
|
}
|
|
996
1035
|
};
|
|
1036
|
+
const reportDecryptRefusals = () => {
|
|
1037
|
+
if (refusalTally.refused === 0) return;
|
|
1038
|
+
logger.warn(DECRYPT_REFUSAL_LOG_MESSAGE, refusalTally.toLogContext(String(entity)));
|
|
1039
|
+
};
|
|
997
1040
|
const normalizeCfJsonAliases = (rows) => {
|
|
998
1041
|
if (cfJsonAliases.size === 0) return;
|
|
999
1042
|
for (const row of rows) {
|
|
@@ -1064,6 +1107,10 @@ class BasicQueryEngine {
|
|
|
1064
1107
|
stripCfSortAliases(items);
|
|
1065
1108
|
pagedItems = decryptPayload ? await mapWithConcurrency(items, DECRYPT_CONCURRENCY, decryptRow) : items;
|
|
1066
1109
|
}
|
|
1110
|
+
reportDecryptRefusals();
|
|
1111
|
+
if (injectsTenantIdForDecrypt) {
|
|
1112
|
+
for (const row of pagedItems) delete row.tenant_id;
|
|
1113
|
+
}
|
|
1067
1114
|
let queryResult = { items: pagedItems, page, pageSize, total };
|
|
1068
1115
|
if (encryptedSortRowCapWarning || listCountCapWarning) {
|
|
1069
1116
|
const meta = {};
|