@open-mercato/shared 0.10.1-develop.7601.1.e057b693a8 → 0.10.1-develop.7616.1.51a1f65492

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
1
  [generate-countries] up to date (259 countries)
2
- [build:shared] found 321 entry points
2
+ [build:shared] found 322 entry points
3
3
  [build:shared] built successfully
@@ -0,0 +1,70 @@
1
+ import { createLogger } from "../logger/index.js";
2
+ import { getEntityIds } from "./entityIds.js";
3
+ const logger = createLogger("shared").child({ component: "encryption" });
4
+ function editDistance(left, right) {
5
+ const distances = Array.from({ length: right.length + 1 }, (_, index) => index);
6
+ for (let row = 1; row <= left.length; row++) {
7
+ let diagonal = distances[0];
8
+ distances[0] = row;
9
+ for (let column = 1; column <= right.length; column++) {
10
+ const above = distances[column];
11
+ distances[column] = Math.min(
12
+ above + 1,
13
+ distances[column - 1] + 1,
14
+ diagonal + (left[row - 1] === right[column - 1] ? 0 : 1)
15
+ );
16
+ diagonal = above;
17
+ }
18
+ }
19
+ return distances[right.length];
20
+ }
21
+ function closestEntityId(entityId, candidates) {
22
+ const rank = (candidate) => [
23
+ candidate.startsWith(entityId) || entityId.startsWith(candidate) ? 0 : 1,
24
+ editDistance(entityId, candidate)
25
+ ];
26
+ let closest = null;
27
+ let closestRank = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
28
+ for (const candidate of candidates) {
29
+ const candidateRank = rank(candidate);
30
+ if (candidateRank[0] < closestRank[0] || candidateRank[0] === closestRank[0] && candidateRank[1] < closestRank[1]) {
31
+ closest = candidate;
32
+ closestRank = candidateRank;
33
+ }
34
+ }
35
+ return closest;
36
+ }
37
+ function findUnresolvedEncryptionMapEntityIds(maps, entityIds) {
38
+ const registered = new Set(Object.values(entityIds).flatMap((moduleEntityIds) => Object.values(moduleEntityIds ?? {})));
39
+ const checked = /* @__PURE__ */ new Set();
40
+ const unresolved = [];
41
+ for (const { entityId } of maps) {
42
+ if (registered.has(entityId) || checked.has(entityId)) continue;
43
+ checked.add(entityId);
44
+ const moduleId = entityId.split(":")[0];
45
+ const moduleEntityIds = Object.values(entityIds[moduleId] ?? {});
46
+ if (!moduleEntityIds.length) continue;
47
+ unresolved.push({ entityId, suggestion: closestEntityId(entityId, moduleEntityIds) });
48
+ }
49
+ return unresolved;
50
+ }
51
+ const warnedEntityIds = /* @__PURE__ */ new Set();
52
+ function warnUnresolvedEncryptionMapEntityIds(maps) {
53
+ for (const { entityId, suggestion } of findUnresolvedEncryptionMapEntityIds(maps, getEntityIds(false))) {
54
+ if (warnedEntityIds.has(entityId)) continue;
55
+ warnedEntityIds.add(entityId);
56
+ logger.warn("Encryption map entityId matches no registered entity - its fields are stored as plaintext", {
57
+ entityId,
58
+ suggestion
59
+ });
60
+ }
61
+ }
62
+ function resetUnresolvedEncryptionMapWarnings() {
63
+ warnedEntityIds.clear();
64
+ }
65
+ export {
66
+ findUnresolvedEncryptionMapEntityIds,
67
+ resetUnresolvedEncryptionMapWarnings,
68
+ warnUnresolvedEncryptionMapEntityIds
69
+ };
70
+ //# sourceMappingURL=unresolvedMapEntityIds.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/encryption/unresolvedMapEntityIds.ts"],
4
+ "sourcesContent": ["import type { ModuleEncryptionMap } from '../../modules/encryption'\nimport { createLogger } from '../logger'\nimport { getEntityIds, type EntityIds } from './entityIds'\n\nconst logger = createLogger('shared').child({ component: 'encryption' })\n\nexport type UnresolvedEncryptionMapEntityId = {\n entityId: string\n suggestion: string | null\n}\n\nfunction editDistance(left: string, right: string): number {\n const distances = Array.from({ length: right.length + 1 }, (_, index) => index)\n for (let row = 1; row <= left.length; row++) {\n let diagonal = distances[0]\n distances[0] = row\n for (let column = 1; column <= right.length; column++) {\n const above = distances[column]\n distances[column] = Math.min(\n above + 1,\n distances[column - 1] + 1,\n diagonal + (left[row - 1] === right[column - 1] ? 0 : 1),\n )\n diagonal = above\n }\n }\n return distances[right.length]\n}\n\nfunction closestEntityId(entityId: string, candidates: string[]): string | null {\n const rank = (candidate: string): [number, number] => [\n candidate.startsWith(entityId) || entityId.startsWith(candidate) ? 0 : 1,\n editDistance(entityId, candidate),\n ]\n let closest: string | null = null\n let closestRank: [number, number] = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]\n for (const candidate of candidates) {\n const candidateRank = rank(candidate)\n if (candidateRank[0] < closestRank[0] || (candidateRank[0] === closestRank[0] && candidateRank[1] < closestRank[1])) {\n closest = candidate\n closestRank = candidateRank\n }\n }\n return closest\n}\n\n/**\n * Lists encryption maps whose `entityId` names a module that registers ORM entities but matches\n * none of them. The encryption subscriber derives the entity id from the entity class, so such a\n * map is never consulted and the fields it declares are stored as plaintext.\n *\n * Ids outside an entity-registering module are left alone: `vector:vector_search`, for example,\n * is encrypted explicitly by the search module rather than through an ORM entity.\n */\nexport function findUnresolvedEncryptionMapEntityIds(\n maps: ReadonlyArray<Pick<ModuleEncryptionMap, 'entityId'>>,\n entityIds: EntityIds,\n): UnresolvedEncryptionMapEntityId[] {\n const registered = new Set(Object.values(entityIds).flatMap((moduleEntityIds) => Object.values(moduleEntityIds ?? {})))\n const checked = new Set<string>()\n const unresolved: UnresolvedEncryptionMapEntityId[] = []\n for (const { entityId } of maps) {\n if (registered.has(entityId) || checked.has(entityId)) continue\n checked.add(entityId)\n const moduleId = entityId.split(':')[0]\n const moduleEntityIds = Object.values(entityIds[moduleId] ?? {})\n if (!moduleEntityIds.length) continue\n unresolved.push({ entityId, suggestion: closestEntityId(entityId, moduleEntityIds) })\n }\n return unresolved\n}\n\nconst warnedEntityIds = new Set<string>()\n\n/**\n * Warns about every unresolved map once per process. The core DI bootstrap that loads the maps\n * can run once per request container, so repeated calls stay quiet.\n */\nexport function warnUnresolvedEncryptionMapEntityIds(maps: ReadonlyArray<Pick<ModuleEncryptionMap, 'entityId'>>): void {\n for (const { entityId, suggestion } of findUnresolvedEncryptionMapEntityIds(maps, getEntityIds(false))) {\n if (warnedEntityIds.has(entityId)) continue\n warnedEntityIds.add(entityId)\n logger.warn('Encryption map entityId matches no registered entity - its fields are stored as plaintext', {\n entityId,\n suggestion,\n })\n }\n}\n\nexport function resetUnresolvedEncryptionMapWarnings(): void {\n warnedEntityIds.clear()\n}\n"],
5
+ "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,oBAAoC;AAE7C,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,aAAa,CAAC;AAOvE,SAAS,aAAa,MAAc,OAAuB;AACzD,QAAM,YAAY,MAAM,KAAK,EAAE,QAAQ,MAAM,SAAS,EAAE,GAAG,CAAC,GAAG,UAAU,KAAK;AAC9E,WAAS,MAAM,GAAG,OAAO,KAAK,QAAQ,OAAO;AAC3C,QAAI,WAAW,UAAU,CAAC;AAC1B,cAAU,CAAC,IAAI;AACf,aAAS,SAAS,GAAG,UAAU,MAAM,QAAQ,UAAU;AACrD,YAAM,QAAQ,UAAU,MAAM;AAC9B,gBAAU,MAAM,IAAI,KAAK;AAAA,QACvB,QAAQ;AAAA,QACR,UAAU,SAAS,CAAC,IAAI;AAAA,QACxB,YAAY,KAAK,MAAM,CAAC,MAAM,MAAM,SAAS,CAAC,IAAI,IAAI;AAAA,MACxD;AACA,iBAAW;AAAA,IACb;AAAA,EACF;AACA,SAAO,UAAU,MAAM,MAAM;AAC/B;AAEA,SAAS,gBAAgB,UAAkB,YAAqC;AAC9E,QAAM,OAAO,CAAC,cAAwC;AAAA,IACpD,UAAU,WAAW,QAAQ,KAAK,SAAS,WAAW,SAAS,IAAI,IAAI;AAAA,IACvE,aAAa,UAAU,SAAS;AAAA,EAClC;AACA,MAAI,UAAyB;AAC7B,MAAI,cAAgC,CAAC,OAAO,mBAAmB,OAAO,iBAAiB;AACvF,aAAW,aAAa,YAAY;AAClC,UAAM,gBAAgB,KAAK,SAAS;AACpC,QAAI,cAAc,CAAC,IAAI,YAAY,CAAC,KAAM,cAAc,CAAC,MAAM,YAAY,CAAC,KAAK,cAAc,CAAC,IAAI,YAAY,CAAC,GAAI;AACnH,gBAAU;AACV,oBAAc;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AAUO,SAAS,qCACd,MACA,WACmC;AACnC,QAAM,aAAa,IAAI,IAAI,OAAO,OAAO,SAAS,EAAE,QAAQ,CAAC,oBAAoB,OAAO,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC;AACtH,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,aAAgD,CAAC;AACvD,aAAW,EAAE,SAAS,KAAK,MAAM;AAC/B,QAAI,WAAW,IAAI,QAAQ,KAAK,QAAQ,IAAI,QAAQ,EAAG;AACvD,YAAQ,IAAI,QAAQ;AACpB,UAAM,WAAW,SAAS,MAAM,GAAG,EAAE,CAAC;AACtC,UAAM,kBAAkB,OAAO,OAAO,UAAU,QAAQ,KAAK,CAAC,CAAC;AAC/D,QAAI,CAAC,gBAAgB,OAAQ;AAC7B,eAAW,KAAK,EAAE,UAAU,YAAY,gBAAgB,UAAU,eAAe,EAAE,CAAC;AAAA,EACtF;AACA,SAAO;AACT;AAEA,MAAM,kBAAkB,oBAAI,IAAY;AAMjC,SAAS,qCAAqC,MAAkE;AACrH,aAAW,EAAE,UAAU,WAAW,KAAK,qCAAqC,MAAM,aAAa,KAAK,CAAC,GAAG;AACtG,QAAI,gBAAgB,IAAI,QAAQ,EAAG;AACnC,oBAAgB,IAAI,QAAQ;AAC5B,WAAO,KAAK,6FAA6F;AAAA,MACvG;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEO,SAAS,uCAA6C;AAC3D,kBAAgB,MAAM;AACxB;",
6
+ "names": []
7
+ }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.10.1-develop.7601.1.e057b693a8";
1
+ const APP_VERSION = "0.10.1-develop.7616.1.51a1f65492";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.10.1-develop.7601.1.e057b693a8';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.10.1-develop.7616.1.51a1f65492';\nexport const appVersion = APP_VERSION;\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.10.1-develop.7601.1.e057b693a8",
3
+ "version": "0.10.1-develop.7616.1.51a1f65492",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -126,7 +126,7 @@
126
126
  "@mikro-orm/core": "^7.1.14",
127
127
  "@mikro-orm/decorators": "^7.1.14",
128
128
  "@mikro-orm/postgresql": "^7.1.14",
129
- "@open-mercato/cache": "0.10.1-develop.7601.1.e057b693a8",
129
+ "@open-mercato/cache": "0.10.1-develop.7616.1.51a1f65492",
130
130
  "@types/html-to-text": "^9.0.4",
131
131
  "@types/sanitize-html": "^2.16.1",
132
132
  "dotenv": "^17.4.2",
@@ -0,0 +1,92 @@
1
+ const mockWarn = jest.fn()
2
+
3
+ jest.mock('../../logger', () => ({
4
+ createLogger: () => ({ child: () => ({ warn: mockWarn }) }),
5
+ }))
6
+
7
+ import { registerEntityIds } from '../entityIds'
8
+ import {
9
+ findUnresolvedEncryptionMapEntityIds,
10
+ resetUnresolvedEncryptionMapWarnings,
11
+ warnUnresolvedEncryptionMapEntityIds,
12
+ } from '../unresolvedMapEntityIds'
13
+
14
+ const entityIds = {
15
+ customers: {
16
+ customer_entity: 'customers:customer_entity',
17
+ customer_deal: 'customers:customer_deal',
18
+ },
19
+ fleet: {
20
+ vehicle_profile: 'fleet:vehicle_profile',
21
+ vehicle_trip: 'fleet:vehicle_trip',
22
+ },
23
+ }
24
+
25
+ describe('findUnresolvedEncryptionMapEntityIds', () => {
26
+ it('accepts maps whose entityId is a registered entity', () => {
27
+ expect(
28
+ findUnresolvedEncryptionMapEntityIds(
29
+ [{ entityId: 'customers:customer_entity' }, { entityId: 'fleet:vehicle_trip' }],
30
+ entityIds,
31
+ ),
32
+ ).toEqual([])
33
+ })
34
+
35
+ it('reports a map that names no entity of a module that registers entities, with the closest match', () => {
36
+ expect(
37
+ findUnresolvedEncryptionMapEntityIds([{ entityId: 'fleet:vehicle' }, { entityId: 'fleet:vehicle' }], entityIds),
38
+ ).toEqual([{ entityId: 'fleet:vehicle', suggestion: 'fleet:vehicle_trip' }])
39
+ expect(findUnresolvedEncryptionMapEntityIds([{ entityId: 'fleet:vehicle_profiles' }], entityIds)).toEqual([
40
+ { entityId: 'fleet:vehicle_profiles', suggestion: 'fleet:vehicle_profile' },
41
+ ])
42
+ })
43
+
44
+ it('prefers an entity that shares the declared prefix over a merely shorter edit', () => {
45
+ const registry = {
46
+ customers: {
47
+ customer_deal: 'customers:customer_deal',
48
+ customer_person_profile: 'customers:customer_person_profile',
49
+ },
50
+ }
51
+
52
+ expect(findUnresolvedEncryptionMapEntityIds([{ entityId: 'customers:customer_person' }], registry)).toEqual([
53
+ { entityId: 'customers:customer_person', suggestion: 'customers:customer_person_profile' },
54
+ ])
55
+ })
56
+
57
+ it('leaves ids outside an entity-registering module alone', () => {
58
+ expect(
59
+ findUnresolvedEncryptionMapEntityIds(
60
+ [{ entityId: 'vector:vector_search' }, { entityId: 'no_colon_id' }],
61
+ entityIds,
62
+ ),
63
+ ).toEqual([])
64
+ })
65
+ })
66
+
67
+ describe('warnUnresolvedEncryptionMapEntityIds', () => {
68
+ beforeEach(() => {
69
+ mockWarn.mockClear()
70
+ resetUnresolvedEncryptionMapWarnings()
71
+ registerEntityIds(entityIds)
72
+ })
73
+
74
+ it('warns once per unresolved map, however often bootstrap runs', () => {
75
+ const maps = [{ entityId: 'fleet:vehicle_profiles' }, { entityId: 'customers:customer_entity' }]
76
+
77
+ warnUnresolvedEncryptionMapEntityIds(maps)
78
+ warnUnresolvedEncryptionMapEntityIds(maps)
79
+
80
+ expect(mockWarn).toHaveBeenCalledTimes(1)
81
+ expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('stored as plaintext'), {
82
+ entityId: 'fleet:vehicle_profiles',
83
+ suggestion: 'fleet:vehicle_profile',
84
+ })
85
+ })
86
+
87
+ it('stays quiet when every map resolves', () => {
88
+ warnUnresolvedEncryptionMapEntityIds([{ entityId: 'customers:customer_deal' }])
89
+
90
+ expect(mockWarn).not.toHaveBeenCalled()
91
+ })
92
+ })
@@ -0,0 +1,92 @@
1
+ import type { ModuleEncryptionMap } from '../../modules/encryption'
2
+ import { createLogger } from '../logger'
3
+ import { getEntityIds, type EntityIds } from './entityIds'
4
+
5
+ const logger = createLogger('shared').child({ component: 'encryption' })
6
+
7
+ export type UnresolvedEncryptionMapEntityId = {
8
+ entityId: string
9
+ suggestion: string | null
10
+ }
11
+
12
+ function editDistance(left: string, right: string): number {
13
+ const distances = Array.from({ length: right.length + 1 }, (_, index) => index)
14
+ for (let row = 1; row <= left.length; row++) {
15
+ let diagonal = distances[0]
16
+ distances[0] = row
17
+ for (let column = 1; column <= right.length; column++) {
18
+ const above = distances[column]
19
+ distances[column] = Math.min(
20
+ above + 1,
21
+ distances[column - 1] + 1,
22
+ diagonal + (left[row - 1] === right[column - 1] ? 0 : 1),
23
+ )
24
+ diagonal = above
25
+ }
26
+ }
27
+ return distances[right.length]
28
+ }
29
+
30
+ function closestEntityId(entityId: string, candidates: string[]): string | null {
31
+ const rank = (candidate: string): [number, number] => [
32
+ candidate.startsWith(entityId) || entityId.startsWith(candidate) ? 0 : 1,
33
+ editDistance(entityId, candidate),
34
+ ]
35
+ let closest: string | null = null
36
+ let closestRank: [number, number] = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
37
+ for (const candidate of candidates) {
38
+ const candidateRank = rank(candidate)
39
+ if (candidateRank[0] < closestRank[0] || (candidateRank[0] === closestRank[0] && candidateRank[1] < closestRank[1])) {
40
+ closest = candidate
41
+ closestRank = candidateRank
42
+ }
43
+ }
44
+ return closest
45
+ }
46
+
47
+ /**
48
+ * Lists encryption maps whose `entityId` names a module that registers ORM entities but matches
49
+ * none of them. The encryption subscriber derives the entity id from the entity class, so such a
50
+ * map is never consulted and the fields it declares are stored as plaintext.
51
+ *
52
+ * Ids outside an entity-registering module are left alone: `vector:vector_search`, for example,
53
+ * is encrypted explicitly by the search module rather than through an ORM entity.
54
+ */
55
+ export function findUnresolvedEncryptionMapEntityIds(
56
+ maps: ReadonlyArray<Pick<ModuleEncryptionMap, 'entityId'>>,
57
+ entityIds: EntityIds,
58
+ ): UnresolvedEncryptionMapEntityId[] {
59
+ const registered = new Set(Object.values(entityIds).flatMap((moduleEntityIds) => Object.values(moduleEntityIds ?? {})))
60
+ const checked = new Set<string>()
61
+ const unresolved: UnresolvedEncryptionMapEntityId[] = []
62
+ for (const { entityId } of maps) {
63
+ if (registered.has(entityId) || checked.has(entityId)) continue
64
+ checked.add(entityId)
65
+ const moduleId = entityId.split(':')[0]
66
+ const moduleEntityIds = Object.values(entityIds[moduleId] ?? {})
67
+ if (!moduleEntityIds.length) continue
68
+ unresolved.push({ entityId, suggestion: closestEntityId(entityId, moduleEntityIds) })
69
+ }
70
+ return unresolved
71
+ }
72
+
73
+ const warnedEntityIds = new Set<string>()
74
+
75
+ /**
76
+ * Warns about every unresolved map once per process. The core DI bootstrap that loads the maps
77
+ * can run once per request container, so repeated calls stay quiet.
78
+ */
79
+ export function warnUnresolvedEncryptionMapEntityIds(maps: ReadonlyArray<Pick<ModuleEncryptionMap, 'entityId'>>): void {
80
+ for (const { entityId, suggestion } of findUnresolvedEncryptionMapEntityIds(maps, getEntityIds(false))) {
81
+ if (warnedEntityIds.has(entityId)) continue
82
+ warnedEntityIds.add(entityId)
83
+ logger.warn('Encryption map entityId matches no registered entity - its fields are stored as plaintext', {
84
+ entityId,
85
+ suggestion,
86
+ })
87
+ }
88
+ }
89
+
90
+ export function resetUnresolvedEncryptionMapWarnings(): void {
91
+ warnedEntityIds.clear()
92
+ }