@open-mercato/core 0.6.5-develop.5187.1.82e5532561 → 0.6.5-develop.5200.1.871eca3402

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,4 +1,4 @@
1
- [build:core] found 3310 entry points
1
+ [build:core] found 3311 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 185 entry points
4
4
  [build:core:generated] built successfully
@@ -1,5 +1,5 @@
1
1
  import { promises as fs } from "fs";
2
- import path from "path";
2
+ import { resolveContainedPath, resolveLegacyPublicRoot } from "../pathContainment.js";
3
3
  class LegacyPublicStorageDriver {
4
4
  constructor() {
5
5
  this.key = "legacyPublic";
@@ -27,13 +27,7 @@ class LegacyPublicStorageDriver {
27
27
  };
28
28
  }
29
29
  resolveAbsolutePath(storagePath) {
30
- let safeRelative = storagePath.replace(/^\/*/, "");
31
- let prev;
32
- do {
33
- prev = safeRelative;
34
- safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, "");
35
- } while (safeRelative !== prev);
36
- return path.join(process.cwd(), safeRelative);
30
+ return resolveContainedPath(process.cwd(), storagePath, resolveLegacyPublicRoot());
37
31
  }
38
32
  }
39
33
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/attachments/lib/drivers/legacyPublicDriver.ts"],
4
- "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\n\nexport class LegacyPublicStorageDriver implements StorageDriver {\n readonly key = 'legacyPublic'\n\n store(_payload: StoreFilePayload): Promise<StoredFile> {\n throw new Error('legacy-public driver is read-only')\n }\n\n async read(_partitionCode: string, storagePath: string): Promise<ReadFileResult> {\n const absolutePath = this.resolveAbsolutePath(storagePath)\n const buffer = await fs.readFile(absolutePath)\n return { buffer }\n }\n\n async delete(_partitionCode: string, storagePath: string): Promise<void> {\n const absolutePath = this.resolveAbsolutePath(storagePath)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n }\n\n async toLocalPath(\n _partitionCode: string,\n storagePath: string,\n ): Promise<{ filePath: string; cleanup: () => Promise<void> }> {\n return {\n filePath: this.resolveAbsolutePath(storagePath),\n cleanup: async () => {},\n }\n }\n\n private resolveAbsolutePath(storagePath: string): string {\n let safeRelative = storagePath.replace(/^\\/*/, '')\n let prev: string\n do {\n prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n return path.join(process.cwd(), safeRelative)\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AAGV,MAAM,0BAAmD;AAAA,EAAzD;AACL,SAAS,MAAM;AAAA;AAAA,EAEf,MAAM,UAAiD;AACrD,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAAA,EAEA,MAAM,KAAK,gBAAwB,aAA8C;AAC/E,UAAM,eAAe,KAAK,oBAAoB,WAAW;AACzD,UAAM,SAAS,MAAM,GAAG,SAAS,YAAY;AAC7C,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,OAAO,gBAAwB,aAAoC;AACvE,UAAM,eAAe,KAAK,oBAAoB,WAAW;AACzD,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,YACJ,gBACA,aAC6D;AAC7D,WAAO;AAAA,MACL,UAAU,KAAK,oBAAoB,WAAW;AAAA,MAC9C,SAAS,YAAY;AAAA,MAAC;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,oBAAoB,aAA6B;AACvD,QAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AACjD,QAAI;AACJ,OAAG;AACD,aAAO;AACP,qBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,IACxD,SAAS,iBAAiB;AAC1B,WAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY;AAAA,EAC9C;AACF;",
4
+ "sourcesContent": ["import { promises as fs } from 'fs'\nimport { resolveContainedPath, resolveLegacyPublicRoot } from '../pathContainment'\nimport type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\n\nexport class LegacyPublicStorageDriver implements StorageDriver {\n readonly key = 'legacyPublic'\n\n store(_payload: StoreFilePayload): Promise<StoredFile> {\n throw new Error('legacy-public driver is read-only')\n }\n\n async read(_partitionCode: string, storagePath: string): Promise<ReadFileResult> {\n const absolutePath = this.resolveAbsolutePath(storagePath)\n const buffer = await fs.readFile(absolutePath)\n return { buffer }\n }\n\n async delete(_partitionCode: string, storagePath: string): Promise<void> {\n const absolutePath = this.resolveAbsolutePath(storagePath)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n }\n\n async toLocalPath(\n _partitionCode: string,\n storagePath: string,\n ): Promise<{ filePath: string; cleanup: () => Promise<void> }> {\n return {\n filePath: this.resolveAbsolutePath(storagePath),\n cleanup: async () => {},\n }\n }\n\n private resolveAbsolutePath(storagePath: string): string {\n return resolveContainedPath(process.cwd(), storagePath, resolveLegacyPublicRoot())\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,SAAS,sBAAsB,+BAA+B;AAGvD,MAAM,0BAAmD;AAAA,EAAzD;AACL,SAAS,MAAM;AAAA;AAAA,EAEf,MAAM,UAAiD;AACrD,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAAA,EAEA,MAAM,KAAK,gBAAwB,aAA8C;AAC/E,UAAM,eAAe,KAAK,oBAAoB,WAAW;AACzD,UAAM,SAAS,MAAM,GAAG,SAAS,YAAY;AAC7C,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,OAAO,gBAAwB,aAAoC;AACvE,UAAM,eAAe,KAAK,oBAAoB,WAAW;AACzD,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,YACJ,gBACA,aAC6D;AAC7D,WAAO;AAAA,MACL,UAAU,KAAK,oBAAoB,WAAW;AAAA,MAC9C,SAAS,YAAY;AAAA,MAAC;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,oBAAoB,aAA6B;AACvD,WAAO,qBAAqB,QAAQ,IAAI,GAAG,aAAa,wBAAwB,CAAC;AAAA,EACnF;AACF;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import { randomUUID } from "crypto";
4
4
  import { resolvePartitionRoot } from "../storage.js";
5
+ import { resolveContainedPath } from "../pathContainment.js";
5
6
  function sanitizeFileName(fileName) {
6
7
  if (!fileName) return "file";
7
8
  return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
@@ -54,14 +55,8 @@ class LocalStorageDriver {
54
55
  };
55
56
  }
56
57
  resolveAbsolutePath(partitionCode, storagePath) {
57
- let safeRelative = storagePath.replace(/^\/*/, "");
58
- let prev;
59
- do {
60
- prev = safeRelative;
61
- safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, "");
62
- } while (safeRelative !== prev);
63
58
  const root = resolvePartitionRoot(partitionCode);
64
- return path.join(root, safeRelative);
59
+ return resolveContainedPath(root, storagePath);
65
60
  }
66
61
  }
67
62
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/attachments/lib/drivers/localDriver.ts"],
4
- "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionRoot } from '../storage'\nimport type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\nexport class LocalStorageDriver implements StorageDriver {\n readonly key = 'local'\n\n async store(payload: StoreFilePayload): Promise<StoredFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n }\n }\n\n async read(partitionCode: string, storagePath: string): Promise<ReadFileResult> {\n const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath)\n const buffer = await fs.readFile(absolutePath)\n return { buffer }\n }\n\n async delete(partitionCode: string, storagePath: string): Promise<void> {\n const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n }\n\n async toLocalPath(\n partitionCode: string,\n storagePath: string,\n ): Promise<{ filePath: string; cleanup: () => Promise<void> }> {\n const filePath = this.resolveAbsolutePath(partitionCode, storagePath)\n return {\n filePath,\n cleanup: async () => {\n // no-op: local path is the real file, do not delete\n },\n }\n }\n\n private resolveAbsolutePath(partitionCode: string, storagePath: string): string {\n let safeRelative = storagePath.replace(/^\\/*/, '')\n let prev: string\n do {\n prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n const root = resolvePartitionRoot(partitionCode)\n return path.join(root, safeRelative)\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,4BAA4B;AAGrC,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAEO,MAAM,mBAA4C;AAAA,EAAlD;AACL,SAAS,MAAM;AAAA;AAAA,EAEf,MAAM,MAAM,SAAgD;AAC1D,UAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,UAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,UAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,UAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,UAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,UAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,UAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,UAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,UAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,UAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,WAAO;AAAA,MACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,eAAuB,aAA8C;AAC9E,UAAM,eAAe,KAAK,oBAAoB,eAAe,WAAW;AACxE,UAAM,SAAS,MAAM,GAAG,SAAS,YAAY;AAC7C,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,OAAO,eAAuB,aAAoC;AACtE,UAAM,eAAe,KAAK,oBAAoB,eAAe,WAAW;AACxE,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,YACJ,eACA,aAC6D;AAC7D,UAAM,WAAW,KAAK,oBAAoB,eAAe,WAAW;AACpE,WAAO;AAAA,MACL;AAAA,MACA,SAAS,YAAY;AAAA,MAErB;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,oBAAoB,eAAuB,aAA6B;AAC9E,QAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AACjD,QAAI;AACJ,OAAG;AACD,aAAO;AACP,qBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,IACxD,SAAS,iBAAiB;AAC1B,UAAM,OAAO,qBAAqB,aAAa;AAC/C,WAAO,KAAK,KAAK,MAAM,YAAY;AAAA,EACrC;AACF;",
4
+ "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionRoot } from '../storage'\nimport { resolveContainedPath } from '../pathContainment'\nimport type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\nexport class LocalStorageDriver implements StorageDriver {\n readonly key = 'local'\n\n async store(payload: StoreFilePayload): Promise<StoredFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n }\n }\n\n async read(partitionCode: string, storagePath: string): Promise<ReadFileResult> {\n const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath)\n const buffer = await fs.readFile(absolutePath)\n return { buffer }\n }\n\n async delete(partitionCode: string, storagePath: string): Promise<void> {\n const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n }\n\n async toLocalPath(\n partitionCode: string,\n storagePath: string,\n ): Promise<{ filePath: string; cleanup: () => Promise<void> }> {\n const filePath = this.resolveAbsolutePath(partitionCode, storagePath)\n return {\n filePath,\n cleanup: async () => {\n // no-op: local path is the real file, do not delete\n },\n }\n }\n\n private resolveAbsolutePath(partitionCode: string, storagePath: string): string {\n const root = resolvePartitionRoot(partitionCode)\n return resolveContainedPath(root, storagePath)\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,4BAA4B;AACrC,SAAS,4BAA4B;AAGrC,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAEO,MAAM,mBAA4C;AAAA,EAAlD;AACL,SAAS,MAAM;AAAA;AAAA,EAEf,MAAM,MAAM,SAAgD;AAC1D,UAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,UAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,UAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,UAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,UAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,UAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,UAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,UAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,UAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,UAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,WAAO;AAAA,MACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,eAAuB,aAA8C;AAC9E,UAAM,eAAe,KAAK,oBAAoB,eAAe,WAAW;AACxE,UAAM,SAAS,MAAM,GAAG,SAAS,YAAY;AAC7C,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,OAAO,eAAuB,aAAoC;AACtE,UAAM,eAAe,KAAK,oBAAoB,eAAe,WAAW;AACxE,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,YACJ,eACA,aAC6D;AAC7D,UAAM,WAAW,KAAK,oBAAoB,eAAe,WAAW;AACpE,WAAO;AAAA,MACL;AAAA,MACA,SAAS,YAAY;AAAA,MAErB;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,oBAAoB,eAAuB,aAA6B;AAC9E,UAAM,OAAO,qBAAqB,aAAa;AAC/C,WAAO,qBAAqB,MAAM,WAAW;AAAA,EAC/C;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,23 @@
1
+ import path from "path";
2
+ function sanitizeStorageRelativePath(storagePath) {
3
+ return String(storagePath ?? "").split(/[\\/]+/).filter((segment) => segment.length > 0 && segment !== "." && segment !== "..").join(path.sep);
4
+ }
5
+ function resolveContainedPath(joinRoot, storagePath, containmentRoot) {
6
+ const base = path.resolve(joinRoot);
7
+ const boundary = path.resolve(containmentRoot ?? joinRoot);
8
+ const candidate = path.resolve(base, sanitizeStorageRelativePath(storagePath));
9
+ const relativeToBoundary = path.relative(boundary, candidate);
10
+ if (relativeToBoundary.startsWith("..") || path.isAbsolute(relativeToBoundary)) {
11
+ throw new Error("[internal] attachment storage path escapes its containment root");
12
+ }
13
+ return candidate;
14
+ }
15
+ function resolveLegacyPublicRoot() {
16
+ return path.join(process.cwd(), "public");
17
+ }
18
+ export {
19
+ resolveContainedPath,
20
+ resolveLegacyPublicRoot,
21
+ sanitizeStorageRelativePath
22
+ };
23
+ //# sourceMappingURL=pathContainment.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/attachments/lib/pathContainment.ts"],
4
+ "sourcesContent": ["import path from 'path'\n\n/**\n * Reduce a stored relative path to safe segments: drop leading slashes, empty\n * segments, `.`, and `..`. The result can never contain a traversal segment,\n * so joining it onto a root can never climb above that root.\n */\nexport function sanitizeStorageRelativePath(storagePath: string): string {\n return String(storagePath ?? '')\n .split(/[\\\\/]+/)\n .filter((segment) => segment.length > 0 && segment !== '.' && segment !== '..')\n .join(path.sep)\n}\n\n/**\n * Resolve a stored path against `joinRoot` and assert the result stays within\n * `containmentRoot` (defaults to `joinRoot`). Throws when the resolved path\n * escapes the boundary \u2014 e.g. a legacy row whose path points outside `public/`.\n */\nexport function resolveContainedPath(\n joinRoot: string,\n storagePath: string,\n containmentRoot?: string,\n): string {\n const base = path.resolve(joinRoot)\n const boundary = path.resolve(containmentRoot ?? joinRoot)\n const candidate = path.resolve(base, sanitizeStorageRelativePath(storagePath))\n const relativeToBoundary = path.relative(boundary, candidate)\n if (relativeToBoundary.startsWith('..') || path.isAbsolute(relativeToBoundary)) {\n throw new Error('[internal] attachment storage path escapes its containment root')\n }\n return candidate\n}\n\n/**\n * The fixed sub-root that `legacyPublic` rows are allowed to resolve within.\n * Stored paths include the `public/` prefix (see Migration20251117181353), so\n * they are joined onto `process.cwd()` but constrained to `process.cwd()/public`.\n */\nexport function resolveLegacyPublicRoot(): string {\n return path.join(process.cwd(), 'public')\n}\n"],
5
+ "mappings": "AAAA,OAAO,UAAU;AAOV,SAAS,4BAA4B,aAA6B;AACvE,SAAO,OAAO,eAAe,EAAE,EAC5B,MAAM,QAAQ,EACd,OAAO,CAAC,YAAY,QAAQ,SAAS,KAAK,YAAY,OAAO,YAAY,IAAI,EAC7E,KAAK,KAAK,GAAG;AAClB;AAOO,SAAS,qBACd,UACA,aACA,iBACQ;AACR,QAAM,OAAO,KAAK,QAAQ,QAAQ;AAClC,QAAM,WAAW,KAAK,QAAQ,mBAAmB,QAAQ;AACzD,QAAM,YAAY,KAAK,QAAQ,MAAM,4BAA4B,WAAW,CAAC;AAC7E,QAAM,qBAAqB,KAAK,SAAS,UAAU,SAAS;AAC5D,MAAI,mBAAmB,WAAW,IAAI,KAAK,KAAK,WAAW,kBAAkB,GAAG;AAC9E,UAAM,IAAI,MAAM,iEAAiE;AAAA,EACnF;AACA,SAAO;AACT;AAOO,SAAS,0BAAkC;AAChD,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ;AAC1C;",
6
+ "names": []
7
+ }
@@ -2,6 +2,7 @@ import { promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import { randomUUID } from "crypto";
4
4
  import { resolvePartitionEnvKey } from "./partitionEnv.js";
5
+ import { resolveContainedPath, resolveLegacyPublicRoot } from "./pathContainment.js";
5
6
  function resolvePartitionRoot(code) {
6
7
  const envKey = resolvePartitionEnvKey(code);
7
8
  const envPath = process.env[envKey];
@@ -40,16 +41,11 @@ async function storePartitionFile(payload) {
40
41
  };
41
42
  }
42
43
  function resolveAttachmentAbsolutePath(partitionCode, storagePath, storageDriver) {
43
- let safeRelative = storagePath.replace(/^\/*/, "");
44
- do {
45
- var prev = safeRelative;
46
- safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, "");
47
- } while (safeRelative !== prev);
48
44
  if (storageDriver === "legacyPublic") {
49
- return path.join(process.cwd(), safeRelative);
45
+ return resolveContainedPath(process.cwd(), storagePath, resolveLegacyPublicRoot());
50
46
  }
51
47
  const root = resolvePartitionRoot(partitionCode);
52
- return path.join(root, safeRelative);
48
+ return resolveContainedPath(root, storagePath);
53
49
  }
54
50
  async function deletePartitionFile(partitionCode, storagePath, storageDriver) {
55
51
  const absolutePath = resolveAttachmentAbsolutePath(partitionCode, storagePath, storageDriver);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/attachments/lib/storage.ts"],
4
- "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionEnvKey } from './partitionEnv'\n\nexport function resolvePartitionRoot(code: string): string {\n const envKey = resolvePartitionEnvKey(code)\n const envPath = process.env[envKey]\n if (envPath && envPath.trim().length > 0) {\n return path.resolve(envPath)\n }\n return path.join(process.cwd(), 'storage', 'attachments', code)\n}\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n * Kept for backward compatibility with external callers.\n */\nexport type StorePartitionFilePayload = {\n partitionCode: string\n orgId: string | null | undefined\n tenantId: string | null | undefined\n fileName: string\n buffer: Buffer\n}\n\nexport type StoredPartitionFile = {\n storagePath: string\n absolutePath: string\n fileName: string\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n */\nexport async function storePartitionFile(payload: StorePartitionFilePayload): Promise<StoredPartitionFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n absolutePath,\n fileName: storedName,\n }\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.read()` / `driver.toLocalPath()` instead.\n */\nexport function resolveAttachmentAbsolutePath(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): string {\n // Remove leading slashes first\n let safeRelative = storagePath.replace(/^\\/*/, '')\n // Remove all ../ (and ..\\) path traversal segments, repeatedly until gone\n do {\n var prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n if (storageDriver === 'legacyPublic') {\n return path.join(process.cwd(), safeRelative)\n }\n const root = resolvePartitionRoot(partitionCode)\n return path.join(root, safeRelative)\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.delete()` instead.\n */\nexport async function deletePartitionFile(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): Promise<void> {\n const absolutePath = resolveAttachmentAbsolutePath(partitionCode, storagePath, storageDriver)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,8BAA8B;AAEhC,SAAS,qBAAqB,MAAsB;AACzD,QAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAM,UAAU,QAAQ,IAAI,MAAM;AAClC,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,WAAO,KAAK,QAAQ,OAAO;AAAA,EAC7B;AACA,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW,eAAe,IAAI;AAChE;AAEA,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAuBA,eAAsB,mBAAmB,SAAkE;AACzG,QAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,QAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,QAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,QAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,QAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,QAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,QAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,QAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,QAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,QAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,SAAO;AAAA,IACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC5C;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAKO,SAAS,8BACd,eACA,aACA,eACQ;AAER,MAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AAEjD,KAAG;AACD,QAAI,OAAO;AACX,mBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,EACxD,SAAS,iBAAiB;AAC1B,MAAI,kBAAkB,gBAAgB;AACpC,WAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY;AAAA,EAC9C;AACA,QAAM,OAAO,qBAAqB,aAAa;AAC/C,SAAO,KAAK,KAAK,MAAM,YAAY;AACrC;AAKA,eAAsB,oBACpB,eACA,aACA,eACe;AACf,QAAM,eAAe,8BAA8B,eAAe,aAAa,aAAa;AAC5F,MAAI;AACF,UAAM,GAAG,OAAO,YAAY;AAAA,EAC9B,QAAQ;AAAA,EAER;AACF;",
4
+ "sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionEnvKey } from './partitionEnv'\nimport { resolveContainedPath, resolveLegacyPublicRoot } from './pathContainment'\n\nexport function resolvePartitionRoot(code: string): string {\n const envKey = resolvePartitionEnvKey(code)\n const envPath = process.env[envKey]\n if (envPath && envPath.trim().length > 0) {\n return path.resolve(envPath)\n }\n return path.join(process.cwd(), 'storage', 'attachments', code)\n}\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n * Kept for backward compatibility with external callers.\n */\nexport type StorePartitionFilePayload = {\n partitionCode: string\n orgId: string | null | undefined\n tenantId: string | null | undefined\n fileName: string\n buffer: Buffer\n}\n\nexport type StoredPartitionFile = {\n storagePath: string\n absolutePath: string\n fileName: string\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n */\nexport async function storePartitionFile(payload: StorePartitionFilePayload): Promise<StoredPartitionFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n absolutePath,\n fileName: storedName,\n }\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.read()` / `driver.toLocalPath()` instead.\n */\nexport function resolveAttachmentAbsolutePath(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): string {\n if (storageDriver === 'legacyPublic') {\n return resolveContainedPath(process.cwd(), storagePath, resolveLegacyPublicRoot())\n }\n const root = resolvePartitionRoot(partitionCode)\n return resolveContainedPath(root, storagePath)\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.delete()` instead.\n */\nexport async function deletePartitionFile(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): Promise<void> {\n const absolutePath = resolveAttachmentAbsolutePath(partitionCode, storagePath, storageDriver)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,8BAA8B;AACvC,SAAS,sBAAsB,+BAA+B;AAEvD,SAAS,qBAAqB,MAAsB;AACzD,QAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAM,UAAU,QAAQ,IAAI,MAAM;AAClC,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,WAAO,KAAK,QAAQ,OAAO;AAAA,EAC7B;AACA,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW,eAAe,IAAI;AAChE;AAEA,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAuBA,eAAsB,mBAAmB,SAAkE;AACzG,QAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,QAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,QAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,QAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,QAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,QAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,QAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,QAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,QAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,QAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,SAAO;AAAA,IACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC5C;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAKO,SAAS,8BACd,eACA,aACA,eACQ;AACR,MAAI,kBAAkB,gBAAgB;AACpC,WAAO,qBAAqB,QAAQ,IAAI,GAAG,aAAa,wBAAwB,CAAC;AAAA,EACnF;AACA,QAAM,OAAO,qBAAqB,aAAa;AAC/C,SAAO,qBAAqB,MAAM,WAAW;AAC/C;AAKA,eAAsB,oBACpB,eACA,aACA,eACe;AACf,QAAM,eAAe,8BAA8B,eAAe,aAAa,aAAa;AAC5F,MAAI;AACF,UAAM,GAAG,OAAO,YAAY;AAAA,EAC9B,QAAQ;AAAA,EAER;AACF;",
6
6
  "names": []
7
7
  }
@@ -273,9 +273,9 @@ async function getLinkedTodo(ctx) {
273
273
  function buildCustomerUrl(kind, id) {
274
274
  if (!id) return null;
275
275
  const encoded = encodeURIComponent(id);
276
- if (kind === "person") return `/backend/customers/people/${encoded}`;
277
- if (kind === "company") return `/backend/customers/companies/${encoded}`;
278
- return `/backend/customers/companies/${encoded}`;
276
+ if (kind === "person") return `/backend/customers/people-v2/${encoded}`;
277
+ if (kind === "company") return `/backend/customers/companies-v2/${encoded}`;
278
+ return `/backend/customers/companies-v2/${encoded}`;
279
279
  }
280
280
  function formatDealValue(record) {
281
281
  const amount = record.value_amount ?? record.valueAmount;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/customers/search.ts"],
4
- "sourcesContent": ["import type { QueryCustomFieldSource, QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type {\n SearchModuleConfig,\n SearchBuildContext,\n SearchResultPresenter,\n SearchResultLink,\n SearchIndexSource,\n} from '@open-mercato/shared/modules/search'\nimport { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from './lib/interactionCompatibility'\n\n// =============================================================================\n// Context Types\n// =============================================================================\n\ntype SearchContext = SearchBuildContext & {\n tenantId: string\n queryEngine?: QueryEngine\n}\n\nfunction assertTenantContext(ctx: SearchBuildContext): asserts ctx is SearchContext {\n if (typeof ctx.tenantId !== 'string' || ctx.tenantId.length === 0) {\n throw new Error('[search.customers] Missing tenantId in search build context')\n }\n}\n\ntype CustomerProfileKind = 'person' | 'company'\n\ntype LoadedCustomerEntity = {\n entity: Record<string, unknown> | null\n customFields: Record<string, unknown>\n}\n\n// =============================================================================\n// Caching\n// =============================================================================\n\nconst entityIdCache = new Map<string, LoadedCustomerEntity | null>()\nconst profileEntityCache = new WeakMap<Record<string, unknown>, Partial<Record<CustomerProfileKind, LoadedCustomerEntity | null>>>()\nconst todoCache = new WeakMap<Record<string, unknown>, unknown>()\n\n// =============================================================================\n// Query Configuration\n// =============================================================================\n\nconst CUSTOMER_ENTITY_FIELDS = [\n 'id',\n 'kind',\n 'display_name',\n 'description',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'owner_user_id',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n 'deleted_at',\n] satisfies string[]\n\nconst CUSTOMER_CUSTOM_FIELD_SOURCES: QueryCustomFieldSource[] = [\n {\n entityId: 'customers:customer_person_profile',\n table: 'customer_people',\n alias: 'person_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n {\n entityId: 'customers:customer_company_profile',\n table: 'customer_companies',\n alias: 'company_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n]\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\nfunction extractCustomFieldMap(source: Record<string, unknown> | null | undefined): Record<string, unknown> {\n if (!source) return {}\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(source)) {\n if (value === undefined) continue\n if (key.startsWith('cf:')) {\n result[key.slice(3)] = value\n } else if (key.startsWith('cf_')) {\n result[key.slice(3)] = value\n }\n }\n return result\n}\n\nfunction normalizeCustomerEntity(row: Record<string, unknown>): Record<string, unknown> {\n const normalized: Record<string, unknown> = {\n id: row.id ?? row.entity_id ?? row.entityId ?? null,\n kind: row.kind ?? null,\n }\n const assign = (snake: string, camel?: string) => {\n const value = row[snake] ?? (camel ? row[camel] : undefined)\n if (value !== undefined) {\n normalized[snake] = value\n if (camel) normalized[camel] = value\n }\n }\n assign('display_name', 'displayName')\n assign('description')\n assign('primary_email', 'primaryEmail')\n assign('primary_phone', 'primaryPhone')\n assign('status')\n assign('lifecycle_stage', 'lifecycleStage')\n assign('owner_user_id', 'ownerUserId')\n assign('source')\n assign('next_interaction_at', 'nextInteractionAt')\n assign('next_interaction_name', 'nextInteractionName')\n assign('next_interaction_ref_id', 'nextInteractionRefId')\n assign('next_interaction_icon', 'nextInteractionIcon')\n assign('next_interaction_color', 'nextInteractionColor')\n assign('organization_id', 'organizationId')\n assign('tenant_id', 'tenantId')\n assign('created_at', 'createdAt')\n assign('updated_at', 'updatedAt')\n assign('deleted_at', 'deletedAt')\n return normalized\n}\n\nfunction getProfileCache(record: Record<string, unknown>): Partial<Record<CustomerProfileKind, LoadedCustomerEntity | null>> {\n let cache = profileEntityCache.get(record)\n if (!cache) {\n cache = {}\n profileEntityCache.set(record, cache)\n }\n return cache\n}\n\nfunction subtractCustomFields(\n primary: Record<string, unknown>,\n secondary: Record<string, unknown>,\n): Record<string, unknown> {\n if (!secondary || Object.keys(secondary).length === 0) return {}\n const primaryKeys = new Set(Object.keys(primary))\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(secondary)) {\n if (!primaryKeys.has(key)) {\n result[key] = value\n }\n }\n return result\n}\n\n// =============================================================================\n// Entity Loading Functions\n// =============================================================================\n\ntype CustomerEntityQueryOptions = {\n entityId?: string | null\n profileKind?: CustomerProfileKind\n profileId?: string | null\n}\n\nasync function loadCustomerEntityBundle(ctx: SearchContext, opts: CustomerEntityQueryOptions): Promise<LoadedCustomerEntity | null> {\n if (!ctx.queryEngine) return null\n const filters: Record<string, unknown> = {}\n const resolvedEntityId = typeof opts.entityId === 'string' && opts.entityId.length ? opts.entityId : null\n const resolvedProfileId =\n opts.profileId != null && String(opts.profileId).trim().length > 0 ? String(opts.profileId).trim() : null\n const shouldJoinProfileSource = Boolean(opts.profileKind && resolvedProfileId && !resolvedEntityId)\n if (resolvedEntityId) {\n filters.id = { $eq: resolvedEntityId }\n }\n if (shouldJoinProfileSource) {\n const alias = opts.profileKind === 'person' ? 'person_profile' : 'company_profile'\n filters[`${alias}.id`] = { $eq: resolvedProfileId }\n }\n if (!Object.keys(filters).length) return null\n try {\n const result = await ctx.queryEngine.query('customers:customer_entity', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? undefined,\n filters,\n includeCustomFields: true,\n ...(shouldJoinProfileSource ? { customFieldSources: CUSTOMER_CUSTOM_FIELD_SOURCES } : {}),\n fields: CUSTOMER_ENTITY_FIELDS,\n page: { page: 1, pageSize: 1 },\n })\n const row = result.items[0] as Record<string, unknown> | undefined\n if (!row) return null\n const entity = normalizeCustomerEntity(row)\n const customFields = extractCustomFieldMap(row)\n return { entity, customFields }\n } catch (error) {\n console.warn('[search.customers] Failed to load customer entity via QueryEngine', {\n entityId: resolvedEntityId ?? null,\n profileKind: opts.profileKind ?? null,\n profileId: resolvedProfileId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n return null\n }\n}\n\nasync function loadCustomerEntityForProfile(ctx: SearchContext, kind: CustomerProfileKind): Promise<LoadedCustomerEntity | null> {\n const cache = getProfileCache(ctx.record)\n if (cache[kind] !== undefined) return cache[kind] ?? null\n const entityIdHint = resolveCustomerEntityId(ctx.record)\n const profileIdRaw = ctx.record.id ?? null\n const profileId = profileIdRaw != null ? String(profileIdRaw) : null\n if (!entityIdHint && !profileId) {\n cache[kind] = null\n return null\n }\n const loaded = await loadCustomerEntityBundle(ctx, {\n entityId: entityIdHint,\n profileKind: kind,\n profileId,\n })\n cache[kind] = loaded ?? null\n const resolvedId = loaded?.entity?.id ?? entityIdHint\n if (resolvedId && typeof resolvedId === 'string') {\n ctx.record.entity_id ??= resolvedId\n ctx.record.entityId ??= resolvedId\n entityIdCache.set(resolvedId, loaded ?? null)\n }\n if (loaded?.entity) {\n if (!ctx.record.entity) ctx.record.entity = loaded.entity\n if (!ctx.record.customer_entity) ctx.record.customer_entity = loaded.entity\n }\n return loaded ?? null\n}\n\nasync function loadCustomerEntityById(ctx: SearchContext, entityId: string | null | undefined): Promise<LoadedCustomerEntity | null> {\n const resolvedId = typeof entityId === 'string' && entityId.length ? entityId : null\n if (!resolvedId) return null\n if (entityIdCache.has(resolvedId)) {\n return entityIdCache.get(resolvedId) ?? null\n }\n const loaded = await loadCustomerEntityBundle(ctx, { entityId: resolvedId })\n entityIdCache.set(resolvedId, loaded ?? null)\n return loaded ?? null\n}\n\nasync function getCustomerEntity(ctx: SearchContext, entityId?: string | null): Promise<Record<string, unknown> | null> {\n const profileCache = profileEntityCache.get(ctx.record)\n if (profileCache) {\n const cached = Object.values(profileCache).find((entry) => {\n if (!entry?.entity) return false\n if (!entityId) return true\n return entry.entity.id === entityId\n })\n if (cached?.entity) return cached.entity\n }\n const inline = getInlineCustomerEntity(ctx.record)\n if (inline && (!entityId || inline.id === entityId)) {\n if (inline.id && typeof inline.id === 'string') {\n entityIdCache.set(inline.id, { entity: inline, customFields: {} })\n }\n return inline\n }\n const resolvedId = entityId ?? resolveCustomerEntityId(ctx.record)\n const loaded = await loadCustomerEntityById(ctx, resolvedId)\n return loaded?.entity ?? null\n}\n\ntype HydratedProfileContext = {\n entity: Record<string, unknown> | null\n entityId: string | null\n profileCustomFields: Record<string, unknown>\n entityCustomFields: Record<string, unknown>\n entityOnlyCustomFields: Record<string, unknown>\n}\n\nasync function hydrateProfileContext(ctx: SearchContext, kind: CustomerProfileKind): Promise<HydratedProfileContext> {\n const profileCustomFields = ctx.customFields ?? {}\n const loaded = await loadCustomerEntityForProfile(ctx, kind)\n let entity = loaded?.entity ?? getInlineCustomerEntity(ctx.record)\n let entityCustomFields = loaded?.customFields ?? {}\n let entityId = (entity?.id as string | undefined) ?? resolveCustomerEntityId(ctx.record)\n if (!entity && entityId) {\n const fetched = await loadCustomerEntityById(ctx, entityId)\n entity = fetched?.entity ?? null\n if (fetched?.customFields) {\n entityCustomFields = Object.keys(entityCustomFields).length ? entityCustomFields : fetched.customFields\n }\n }\n if (!entity && !entityId) {\n entityId = resolveCustomerEntityId(ctx.record)\n }\n if (entity?.id && typeof entity.id === 'string') {\n entityId = entity.id\n ctx.record.entity_id ??= entity.id\n ctx.record.entityId ??= entity.id\n if (!ctx.record.entity) ctx.record.entity = entity\n if (!ctx.record.customer_entity) ctx.record.customer_entity = entity\n }\n const entityOnlyCustomFields = subtractCustomFields(profileCustomFields, entityCustomFields)\n return {\n entity: entity ?? null,\n entityId: entityId ?? null,\n profileCustomFields,\n entityCustomFields,\n entityOnlyCustomFields,\n }\n}\n\nasync function loadRecord(ctx: SearchContext, entityId: string, recordId?: string | null) {\n if (!recordId || !ctx.queryEngine) return null\n const res = await ctx.queryEngine.query(entityId, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? undefined,\n filters: { id: recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n })\n return res.items[0] as Record<string, unknown> | undefined\n}\n\nfunction resolveCustomerEntityId(record: Record<string, unknown>): string | null {\n const direct =\n record.customer_entity_id ??\n record.entityId ??\n record.entity_id ??\n record.customerEntityId ??\n record.customerEntityID ??\n (typeof record.entity === 'object' && record.entity ? (record.entity as Record<string, unknown>).id : undefined) ??\n (typeof record.customer_entity === 'object' && record.customer_entity ? (record.customer_entity as Record<string, unknown>).id : undefined)\n const value = typeof direct === 'string' && direct.length ? direct : null\n return value\n}\n\nfunction getInlineCustomerEntity(record: Record<string, unknown>): Record<string, unknown> | null {\n const inline =\n (typeof record.entity === 'object' && record.entity) ||\n (typeof record.customer_entity === 'object' && record.customer_entity) ||\n null\n return inline as Record<string, unknown> | null\n}\n\nasync function getLinkedTodo(ctx: SearchContext) {\n if (todoCache.has(ctx.record)) {\n return todoCache.get(ctx.record)\n }\n const sourceRaw = typeof ctx.record.todo_source === 'string' ? ctx.record.todo_source : EXAMPLE_TODO_SOURCE\n const [moduleId, entityName] = sourceRaw.split(':')\n const entityId = moduleId && entityName ? `${moduleId}:${entityName}` : CUSTOMER_INTERACTION_TASK_SOURCE\n const todo = await loadRecord(ctx, entityId, ctx.record.todo_id as string ?? ctx.record.todoId as string)\n todoCache.set(ctx.record, todo ?? null)\n return todo ?? null\n}\n\n// =============================================================================\n// URL and Formatting Helpers\n// =============================================================================\n\nfunction buildCustomerUrl(kind: string | null | undefined, id?: string | null): string | null {\n if (!id) return null\n const encoded = encodeURIComponent(id)\n if (kind === 'person') return `/backend/customers/people/${encoded}`\n if (kind === 'company') return `/backend/customers/companies/${encoded}`\n return `/backend/customers/companies/${encoded}`\n}\n\nfunction formatDealValue(record: Record<string, unknown>): string | undefined {\n const amount = record.value_amount ?? record.valueAmount\n if (!amount) return undefined\n const currency = record.value_currency ?? record.valueCurrency ?? ''\n return currency ? `${amount} ${currency}` : String(amount)\n}\n\nfunction snippet(text: unknown, max = 140): string | undefined {\n if (typeof text !== 'string') return undefined\n const trimmed = text.trim()\n if (!trimmed.length) return undefined\n if (trimmed.length <= max) return trimmed\n return `${trimmed.slice(0, max - 3)}...`\n}\n\nfunction appendLine(lines: string[], label: string, value: unknown) {\n if (value === null || value === undefined) return\n const text = Array.isArray(value)\n ? value.map((item) => (item === null || item === undefined ? '' : String(item))).filter(Boolean).join(', ')\n : (typeof value === 'object' ? JSON.stringify(value) : String(value))\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction friendlyLabel(input: string): string {\n return input\n .replace(/^cf:/, '')\n .replace(/_/g, ' ')\n .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a} ${b}`)\n .replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\nfunction appendCustomFieldLines(lines: string[], customFields: Record<string, unknown>, prefix: string) {\n for (const [key, value] of Object.entries(customFields)) {\n if (value === null || value === undefined) continue\n const label = prefix ? `${prefix} ${friendlyLabel(key)}` : friendlyLabel(key)\n appendLine(lines, label, value)\n }\n}\n\nfunction pickValue(source: Record<string, unknown> | null | undefined, ...keys: string[]): unknown {\n if (!source) return undefined\n for (const key of keys) {\n if (key in source && source[key] != null) return source[key]\n }\n return undefined\n}\n\nfunction pickString(...candidates: unknown[]): string | null {\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim().length) {\n return candidate.trim()\n }\n }\n return null\n}\n\nfunction pickLabel(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n if (candidate === null || candidate === undefined) continue\n const value = typeof candidate === 'string' ? candidate : String(candidate)\n const trimmed = value.trim()\n if (trimmed.length) return trimmed\n }\n return null\n}\n\nfunction appendCustomerEntityLines(\n lines: string[],\n entity: Record<string, unknown> | null,\n contactLabel: 'Customer' | 'Primary' = 'Customer',\n) {\n if (!entity) return\n appendLine(lines, 'Customer', pickValue(entity, 'display_name', 'displayName') ?? entity.id)\n appendLine(lines, `${contactLabel} email`, pickValue(entity, 'primary_email', 'primaryEmail'))\n appendLine(lines, `${contactLabel} phone`, pickValue(entity, 'primary_phone', 'primaryPhone'))\n appendLine(lines, 'Lifecycle stage', pickValue(entity, 'lifecycle_stage', 'lifecycleStage'))\n appendLine(lines, 'Status', pickValue(entity, 'status'))\n}\n\nfunction ensureFallbackLines(lines: string[], record: Record<string, unknown>, options: { includeId?: boolean } = {}) {\n if (lines.length) return\n const excluded = new Set(['tenant_id', 'organization_id', 'created_at', 'updated_at', 'deleted_at'])\n for (const [key, value] of Object.entries(record)) {\n if (value === null || value === undefined) continue\n if (excluded.has(key)) continue\n if (key === 'id') continue\n appendLine(lines, friendlyLabel(key), value)\n }\n if (!lines.length && options.includeId !== false) {\n const fallbackId =\n record.id ??\n record.entity_id ??\n record.customer_entity_id ??\n record.entityId ??\n record.customerEntityId ??\n null\n if (fallbackId) {\n appendLine(lines, 'Record ID', fallbackId)\n }\n }\n}\n\n// =============================================================================\n// Presenter Functions\n// =============================================================================\n\nfunction resolvePersonPresenter(\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n customFields: Record<string, unknown>,\n): SearchResultPresenter {\n const fallbackEntityId = resolveCustomerEntityId(record)\n const firstName = record.first_name ?? record.firstName ?? customFields.first_name ?? customFields.firstName ?? ''\n const lastName = record.last_name ?? record.lastName ?? customFields.last_name ?? customFields.lastName ?? ''\n const nameParts = [firstName, lastName].filter(Boolean).join(' ')\n const title =\n (pickValue(entity, 'display_name', 'displayName') as string | undefined) ??\n (record.preferred_name as string | undefined) ??\n (record.preferredName as string | undefined) ??\n (nameParts.length ? nameParts : undefined) ??\n fallbackEntityId ??\n (record.id as string | undefined) ??\n 'Person'\n const subtitlePieces: string[] = []\n const jobTitle = record.job_title ?? record.jobTitle ?? customFields.job_title ?? customFields.jobTitle\n if (jobTitle) subtitlePieces.push(String(jobTitle))\n const department = record.department ?? customFields.department\n if (department) subtitlePieces.push(String(department))\n const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')\n if (primaryEmail) subtitlePieces.push(String(primaryEmail))\n const primaryPhone = pickValue(entity, 'primary_phone', 'primaryPhone')\n if (primaryPhone) subtitlePieces.push(String(primaryPhone))\n const summary = snippet(\n (pickValue(entity, 'description') as string | undefined) ??\n (customFields.summary as string | undefined) ??\n (customFields.description as string | undefined),\n )\n if (summary) subtitlePieces.push(summary)\n return {\n title: String(title),\n subtitle: subtitlePieces.length ? subtitlePieces.join(' \u00B7 ') : undefined,\n icon: 'user',\n badge: pickValue(entity, 'display_name', 'displayName') ? 'Person' : undefined,\n }\n}\n\nfunction resolveCompanyPresenter(\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n customFields: Record<string, unknown>,\n): SearchResultPresenter {\n const fallbackEntityId = resolveCustomerEntityId(record)\n const title =\n (pickValue(entity, 'display_name', 'displayName') as string | undefined) ??\n (customFields.display_name as string | undefined) ??\n (customFields.displayName as string | undefined) ??\n (record.brand_name as string | undefined) ??\n (record.legal_name as string | undefined) ??\n (record.domain as string | undefined) ??\n (record.brandName as string | undefined) ??\n (record.legalName as string | undefined) ??\n (entity?.id && entity?.display_name ? entity.display_name as string : undefined) ??\n fallbackEntityId ??\n (record.id as string | undefined) ??\n 'Company'\n const subtitlePieces: string[] = []\n const industry = record.industry\n if (industry) subtitlePieces.push(String(industry))\n const sizeBucket = record.size_bucket ?? record.sizeBucket\n if (sizeBucket) subtitlePieces.push(String(sizeBucket))\n if (entity) {\n const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')\n if (primaryEmail) subtitlePieces.push(String(primaryEmail))\n }\n const summary = snippet(\n (pickValue(entity, 'description') as string | undefined) ??\n (customFields.summary as string | undefined) ??\n (customFields.description as string | undefined) ??\n (record.summary as string | undefined) ??\n (record.description as string | undefined),\n )\n if (summary) subtitlePieces.push(summary)\n if (!entity && (!title || title === fallbackEntityId)) {\n console.warn('[search.customers] Missing customer entity during company presenter build', {\n recordId: record.id ?? null,\n entityId: fallbackEntityId,\n recordKeys: Object.keys(record),\n })\n }\n return {\n title: String(title),\n subtitle: subtitlePieces.length ? subtitlePieces.join(' \u00B7 ') : undefined,\n icon: 'building',\n badge: pickValue(entity, 'display_name', 'displayName') ? 'Company' : undefined,\n }\n}\n\nfunction logMissingPresenterTitle(\n kind: 'person' | 'company',\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n presenter: SearchResultPresenter,\n) {\n const fallbackId = record.id ?? record.entity_id ?? resolveCustomerEntityId(record)\n if (!fallbackId) return\n if (presenter.title && presenter.title !== String(fallbackId)) return\n console.warn('[search.customers] Presenter fell back to record id', {\n kind,\n recordId: fallbackId,\n entityId: resolveCustomerEntityId(record),\n entityDisplayName: entity?.display_name ?? null,\n })\n}\n\n// =============================================================================\n// Search Module Configuration\n// =============================================================================\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n // =========================================================================\n // Person Profile\n // =========================================================================\n {\n entityId: 'customers:customer_person_profile',\n enabled: true,\n priority: 10,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Preferred name', record.preferred_name ?? record.preferredName ?? ctx.customFields.preferred_name)\n appendLine(lines, 'First name', record.first_name ?? record.firstName ?? ctx.customFields.first_name)\n appendLine(lines, 'Last name', record.last_name ?? record.lastName ?? ctx.customFields.last_name)\n appendLine(lines, 'Job title', record.job_title ?? record.jobTitle ?? ctx.customFields.job_title)\n appendLine(lines, 'Department', record.department ?? record.department_name ?? record.departmentName ?? ctx.customFields.department)\n appendLine(lines, 'Seniority', record.seniority ?? record.seniority_level ?? record.seniorityLevel ?? ctx.customFields.seniority)\n appendLine(lines, 'Timezone', record.timezone ?? record.time_zone ?? record.timeZone ?? ctx.customFields.timezone)\n appendLine(lines, 'LinkedIn', record.linked_in_url ?? record.linkedInUrl ?? ctx.customFields.linked_in_url)\n appendLine(lines, 'Twitter', record.twitter_url ?? record.twitterUrl ?? ctx.customFields.twitter_url)\n\n const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =\n await hydrateProfileContext(ctx, 'person')\n appendCustomFieldLines(lines, profileCustomFields, 'Person custom')\n if (Object.keys(entityOnlyCustomFields).length) {\n appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')\n }\n if (!entity) {\n console.warn('[search.customers] Failed to load customer entity for person profile', {\n recordId: record.id,\n entityId,\n recordKeys: Object.keys(record),\n })\n }\n appendCustomerEntityLines(lines, entity, 'Customer')\n ensureFallbackLines(lines, record)\n if (!lines.length) return null\n\n if (!entityId) {\n console.warn('[search.customers] person profile missing entity id', {\n recordId: record.id,\n recordKeys: Object.keys(record),\n })\n }\n\n const presenter = resolvePersonPresenter(record, entity, ctx.customFields)\n logMissingPresenterTitle('person', record, entity, presenter)\n const presenterLabel = pickLabel(presenter.title) ?? 'Open person'\n const links: SearchResultLink[] = []\n if (entityId) {\n const href = buildCustomerUrl('person', entityId)\n if (href) {\n links.push({ href, label: presenterLabel, kind: 'primary' })\n }\n }\n\n return {\n text: lines,\n presenter,\n links,\n checksumSource: {\n record: ctx.record,\n customFields: profileCustomFields,\n entity,\n entityCustomFields,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))\n return resolvePersonPresenter(ctx.record, entity, ctx.customFields)\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n return buildCustomerUrl('person', entityId)\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n if (!entityId) return null\n const href = buildCustomerUrl('person', entityId)\n if (!href) return null\n return [{ href: `${href}/edit`, label: 'Edit', kind: 'secondary' }]\n },\n\n fieldPolicy: {\n searchable: [\n 'preferred_name',\n 'first_name',\n 'last_name',\n 'job_title',\n 'department',\n 'seniority',\n 'timezone',\n 'linked_in_url',\n 'twitter_url',\n ],\n hashOnly: ['primary_email', 'primary_phone', 'personal_email'],\n excluded: ['date_of_birth', 'government_id', 'ssn', 'tax_id'],\n },\n aclFeatures: ['customers.people.view'],\n },\n\n // =========================================================================\n // Company Profile\n // =========================================================================\n {\n entityId: 'customers:customer_company_profile',\n enabled: true,\n priority: 10,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Legal name', record.legal_name ?? record.legalName ?? ctx.customFields.legal_name)\n appendLine(lines, 'Brand name', record.brand_name ?? record.brandName ?? ctx.customFields.brand_name)\n appendLine(lines, 'Domain', record.domain ?? record.website_domain ?? record.websiteDomain ?? ctx.customFields.domain)\n appendLine(lines, 'Website', record.website_url ?? record.websiteUrl ?? ctx.customFields.website_url)\n appendLine(lines, 'Industry', record.industry ?? ctx.customFields.industry)\n appendLine(lines, 'Company size', record.size_bucket ?? record.sizeBucket ?? ctx.customFields.size_bucket)\n appendLine(lines, 'Annual revenue', record.annual_revenue ?? record.annualRevenue ?? ctx.customFields.annual_revenue)\n\n const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =\n await hydrateProfileContext(ctx, 'company')\n appendCustomFieldLines(lines, profileCustomFields, 'Company custom')\n if (Object.keys(entityOnlyCustomFields).length) {\n appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')\n }\n appendCustomerEntityLines(lines, entity, 'Primary')\n ensureFallbackLines(lines, record)\n if (!lines.length) return null\n\n const presenter = resolveCompanyPresenter(record, entity, ctx.customFields)\n logMissingPresenterTitle('company', record, entity, presenter)\n const primaryLabel = pickLabel(presenter.title) ?? 'Open company'\n const links: SearchResultLink[] = []\n if (entityId) {\n const href = buildCustomerUrl('company', entityId)\n if (href) {\n links.push({ href, label: primaryLabel, kind: 'primary' })\n }\n }\n\n return {\n text: lines,\n presenter,\n links,\n checksumSource: {\n record: ctx.record,\n customFields: profileCustomFields,\n entity,\n entityCustomFields,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))\n return resolveCompanyPresenter(ctx.record, entity, ctx.customFields)\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n return buildCustomerUrl('company', entityId)\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n if (!entityId) return null\n const href = buildCustomerUrl('company', entityId)\n if (!href) return null\n return [{ href: `${href}/edit`, label: 'Edit', kind: 'secondary' }]\n },\n\n fieldPolicy: {\n searchable: [\n 'legal_name',\n 'brand_name',\n 'display_name',\n 'domain',\n 'website_url',\n 'industry',\n 'size_bucket',\n 'description',\n ],\n hashOnly: ['tax_id', 'registration_number'],\n excluded: ['bank_account', 'billing_info', 'credit_info'],\n },\n aclFeatures: ['customers.companies.view'],\n },\n\n // =========================================================================\n // Customer Comment\n // =========================================================================\n {\n entityId: 'customers:customer_comment',\n enabled: true,\n priority: 6,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n lines.push(`Note: ${ctx.record.body ?? ''}`)\n if (ctx.record.appearance_icon) lines.push(`Icon: ${ctx.record.appearance_icon}`)\n if (ctx.record.appearance_color) lines.push(`Color: ${ctx.record.appearance_color}`)\n\n const presenter: SearchResultPresenter | undefined = parent?.display_name\n ? {\n title: parent.display_name as string,\n subtitle: snippet(ctx.record.body),\n icon: parent.kind === 'person' ? 'user' : 'building',\n }\n : undefined\n\n return {\n text: lines,\n presenter,\n checksumSource: {\n body: ctx.record.body,\n entityId: ctx.record.entity_id ?? null,\n updatedAt: ctx.record.updated_at ?? ctx.record.updatedAt ?? null,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const title = (parent?.display_name as string | undefined) ?? 'Customer note'\n return {\n title,\n subtitle: snippet(ctx.record.body),\n icon: 'sticky-note',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#notes` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n assertTenantContext(ctx)\n const links: SearchResultLink[] = []\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const parentUrl = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n if (parentUrl) {\n links.push({ href: parentUrl, label: (parent?.display_name as string | undefined) ?? 'View customer', kind: 'primary' })\n }\n if (ctx.record.deal_id) {\n const dealUrl = `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`\n links.push({ href: dealUrl, label: 'Open deal', kind: 'secondary' })\n }\n return links.length ? links : null\n },\n\n fieldPolicy: {\n searchable: ['body'],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n\n // =========================================================================\n // Customer Deal\n // =========================================================================\n {\n entityId: 'customers:customer_deal',\n enabled: true,\n priority: 8,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Stage', record.pipeline_stage)\n appendLine(lines, 'Status', record.status)\n appendLine(lines, 'Source', record.source)\n const value = formatDealValue(record)\n if (value) appendLine(lines, 'Value', value)\n if (!lines.length) return null\n\n const subtitleParts: string[] = []\n if (record.pipeline_stage) subtitleParts.push(String(record.pipeline_stage))\n if (record.status) subtitleParts.push(String(record.status))\n if (value) subtitleParts.push(value)\n\n return {\n text: lines,\n presenter: {\n title: String(record.title ?? 'Deal'),\n subtitle: subtitleParts.join(' \u00B7 ') || undefined,\n icon: 'briefcase',\n badge: 'Deal',\n },\n checksumSource: {\n title: record.title,\n status: record.status,\n stage: record.pipeline_stage,\n value: value,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n const { record } = ctx\n const title = pickString(record.title as string, 'Deal')\n const subtitleParts: string[] = []\n if (record.pipeline_stage) subtitleParts.push(String(record.pipeline_stage))\n if (record.status) subtitleParts.push(String(record.status))\n const amount = record.value_amount ?? record.valueAmount\n const currency = record.value_currency ?? record.valueCurrency\n if (amount) {\n subtitleParts.push(currency ? `${amount} ${currency}` : String(amount))\n }\n\n return {\n title: title ?? 'Deal',\n subtitle: subtitleParts.length ? subtitleParts.join(' \u00B7 ') : undefined,\n icon: 'briefcase',\n badge: 'Deal',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const id = ctx.record.id\n if (!id) return null\n return `/backend/customers/deals/${encodeURIComponent(String(id))}`\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const id = ctx.record.id\n if (!id) return null\n return [\n {\n href: `/backend/customers/deals/${encodeURIComponent(String(id))}/edit`,\n label: 'Edit',\n kind: 'secondary',\n },\n ]\n },\n\n fieldPolicy: {\n searchable: ['title', 'description', 'pipeline_stage', 'status', 'source'],\n hashOnly: [],\n excluded: ['value_amount', 'value_currency'],\n },\n aclFeatures: ['customers.deals.view'],\n },\n\n // =========================================================================\n // Customer Activity\n // =========================================================================\n {\n entityId: 'customers:customer_activity',\n enabled: true,\n priority: 5,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n if (ctx.record.activity_type) lines.push(`Type: ${ctx.record.activity_type}`)\n if (ctx.record.subject) lines.push(`Subject: ${ctx.record.subject}`)\n if (ctx.record.body) lines.push(`Body: ${ctx.record.body}`)\n\n const presenter: SearchResultPresenter = {\n title: ctx.record.subject ? String(ctx.record.subject) : `Activity: ${ctx.record.activity_type ?? 'update'}`,\n subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),\n icon: 'bolt',\n }\n\n return {\n text: lines,\n presenter,\n checksumSource: {\n subject: ctx.record.subject,\n body: ctx.record.body,\n entityId: ctx.record.entity_id ?? null,\n updatedAt: ctx.record.updated_at ?? ctx.record.updatedAt ?? null,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n return {\n title: ctx.record.subject ? String(ctx.record.subject) : `Activity: ${ctx.record.activity_type ?? 'update'}`,\n subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),\n icon: 'bolt',\n badge: 'Activity',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#activity-${ctx.record.id ?? ctx.record.activity_id ?? ''}` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const links: SearchResultLink[] = []\n if (ctx.record.deal_id) {\n links.push({\n href: `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`,\n label: 'Open deal',\n kind: 'secondary',\n })\n }\n return links.length ? links : null\n },\n\n fieldPolicy: {\n searchable: ['subject', 'body', 'activity_type'],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n\n // =========================================================================\n // Customer Todo Link\n // =========================================================================\n {\n entityId: 'customers:customer_todo_link',\n enabled: true,\n priority: 4,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const todo = await getLinkedTodo(ctx) as Record<string, unknown> | null\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (todo?.title) lines.push(`Todo: ${todo.title}`)\n if (todo?.is_done !== undefined) lines.push(`Status: ${todo.is_done ? 'Done' : 'Open'}`)\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n if (!lines.length) return null\n\n return {\n text: lines,\n presenter: todo?.title\n ? { title: todo.title as string, subtitle: parent?.display_name as string | undefined, icon: 'check-square' }\n : undefined,\n checksumSource: {\n todoId: ctx.record.todo_id ?? ctx.record.todoId,\n todoSource: ctx.record.todo_source ?? ctx.record.todoSource,\n entityId: ctx.record.entity_id ?? ctx.record.entityId,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const todo = await getLinkedTodo(ctx) as Record<string, unknown> | null\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n return {\n title: (todo?.title as string | undefined) ?? 'Customer task',\n subtitle: parent?.display_name as string | undefined,\n icon: 'check-square',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#tasks` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const todoId = ctx.record.todo_id ?? ctx.record.todoId\n if (!todoId) return null\n return [{\n href: `/backend/todos/${encodeURIComponent(todoId as string)}/edit`,\n label: 'Open todo',\n kind: 'secondary',\n }]\n },\n\n fieldPolicy: {\n searchable: [],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { QueryCustomFieldSource, QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type {\n SearchModuleConfig,\n SearchBuildContext,\n SearchResultPresenter,\n SearchResultLink,\n SearchIndexSource,\n} from '@open-mercato/shared/modules/search'\nimport { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from './lib/interactionCompatibility'\n\n// =============================================================================\n// Context Types\n// =============================================================================\n\ntype SearchContext = SearchBuildContext & {\n tenantId: string\n queryEngine?: QueryEngine\n}\n\nfunction assertTenantContext(ctx: SearchBuildContext): asserts ctx is SearchContext {\n if (typeof ctx.tenantId !== 'string' || ctx.tenantId.length === 0) {\n throw new Error('[search.customers] Missing tenantId in search build context')\n }\n}\n\ntype CustomerProfileKind = 'person' | 'company'\n\ntype LoadedCustomerEntity = {\n entity: Record<string, unknown> | null\n customFields: Record<string, unknown>\n}\n\n// =============================================================================\n// Caching\n// =============================================================================\n\nconst entityIdCache = new Map<string, LoadedCustomerEntity | null>()\nconst profileEntityCache = new WeakMap<Record<string, unknown>, Partial<Record<CustomerProfileKind, LoadedCustomerEntity | null>>>()\nconst todoCache = new WeakMap<Record<string, unknown>, unknown>()\n\n// =============================================================================\n// Query Configuration\n// =============================================================================\n\nconst CUSTOMER_ENTITY_FIELDS = [\n 'id',\n 'kind',\n 'display_name',\n 'description',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'owner_user_id',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n 'deleted_at',\n] satisfies string[]\n\nconst CUSTOMER_CUSTOM_FIELD_SOURCES: QueryCustomFieldSource[] = [\n {\n entityId: 'customers:customer_person_profile',\n table: 'customer_people',\n alias: 'person_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n {\n entityId: 'customers:customer_company_profile',\n table: 'customer_companies',\n alias: 'company_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n]\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\nfunction extractCustomFieldMap(source: Record<string, unknown> | null | undefined): Record<string, unknown> {\n if (!source) return {}\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(source)) {\n if (value === undefined) continue\n if (key.startsWith('cf:')) {\n result[key.slice(3)] = value\n } else if (key.startsWith('cf_')) {\n result[key.slice(3)] = value\n }\n }\n return result\n}\n\nfunction normalizeCustomerEntity(row: Record<string, unknown>): Record<string, unknown> {\n const normalized: Record<string, unknown> = {\n id: row.id ?? row.entity_id ?? row.entityId ?? null,\n kind: row.kind ?? null,\n }\n const assign = (snake: string, camel?: string) => {\n const value = row[snake] ?? (camel ? row[camel] : undefined)\n if (value !== undefined) {\n normalized[snake] = value\n if (camel) normalized[camel] = value\n }\n }\n assign('display_name', 'displayName')\n assign('description')\n assign('primary_email', 'primaryEmail')\n assign('primary_phone', 'primaryPhone')\n assign('status')\n assign('lifecycle_stage', 'lifecycleStage')\n assign('owner_user_id', 'ownerUserId')\n assign('source')\n assign('next_interaction_at', 'nextInteractionAt')\n assign('next_interaction_name', 'nextInteractionName')\n assign('next_interaction_ref_id', 'nextInteractionRefId')\n assign('next_interaction_icon', 'nextInteractionIcon')\n assign('next_interaction_color', 'nextInteractionColor')\n assign('organization_id', 'organizationId')\n assign('tenant_id', 'tenantId')\n assign('created_at', 'createdAt')\n assign('updated_at', 'updatedAt')\n assign('deleted_at', 'deletedAt')\n return normalized\n}\n\nfunction getProfileCache(record: Record<string, unknown>): Partial<Record<CustomerProfileKind, LoadedCustomerEntity | null>> {\n let cache = profileEntityCache.get(record)\n if (!cache) {\n cache = {}\n profileEntityCache.set(record, cache)\n }\n return cache\n}\n\nfunction subtractCustomFields(\n primary: Record<string, unknown>,\n secondary: Record<string, unknown>,\n): Record<string, unknown> {\n if (!secondary || Object.keys(secondary).length === 0) return {}\n const primaryKeys = new Set(Object.keys(primary))\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(secondary)) {\n if (!primaryKeys.has(key)) {\n result[key] = value\n }\n }\n return result\n}\n\n// =============================================================================\n// Entity Loading Functions\n// =============================================================================\n\ntype CustomerEntityQueryOptions = {\n entityId?: string | null\n profileKind?: CustomerProfileKind\n profileId?: string | null\n}\n\nasync function loadCustomerEntityBundle(ctx: SearchContext, opts: CustomerEntityQueryOptions): Promise<LoadedCustomerEntity | null> {\n if (!ctx.queryEngine) return null\n const filters: Record<string, unknown> = {}\n const resolvedEntityId = typeof opts.entityId === 'string' && opts.entityId.length ? opts.entityId : null\n const resolvedProfileId =\n opts.profileId != null && String(opts.profileId).trim().length > 0 ? String(opts.profileId).trim() : null\n const shouldJoinProfileSource = Boolean(opts.profileKind && resolvedProfileId && !resolvedEntityId)\n if (resolvedEntityId) {\n filters.id = { $eq: resolvedEntityId }\n }\n if (shouldJoinProfileSource) {\n const alias = opts.profileKind === 'person' ? 'person_profile' : 'company_profile'\n filters[`${alias}.id`] = { $eq: resolvedProfileId }\n }\n if (!Object.keys(filters).length) return null\n try {\n const result = await ctx.queryEngine.query('customers:customer_entity', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? undefined,\n filters,\n includeCustomFields: true,\n ...(shouldJoinProfileSource ? { customFieldSources: CUSTOMER_CUSTOM_FIELD_SOURCES } : {}),\n fields: CUSTOMER_ENTITY_FIELDS,\n page: { page: 1, pageSize: 1 },\n })\n const row = result.items[0] as Record<string, unknown> | undefined\n if (!row) return null\n const entity = normalizeCustomerEntity(row)\n const customFields = extractCustomFieldMap(row)\n return { entity, customFields }\n } catch (error) {\n console.warn('[search.customers] Failed to load customer entity via QueryEngine', {\n entityId: resolvedEntityId ?? null,\n profileKind: opts.profileKind ?? null,\n profileId: resolvedProfileId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n return null\n }\n}\n\nasync function loadCustomerEntityForProfile(ctx: SearchContext, kind: CustomerProfileKind): Promise<LoadedCustomerEntity | null> {\n const cache = getProfileCache(ctx.record)\n if (cache[kind] !== undefined) return cache[kind] ?? null\n const entityIdHint = resolveCustomerEntityId(ctx.record)\n const profileIdRaw = ctx.record.id ?? null\n const profileId = profileIdRaw != null ? String(profileIdRaw) : null\n if (!entityIdHint && !profileId) {\n cache[kind] = null\n return null\n }\n const loaded = await loadCustomerEntityBundle(ctx, {\n entityId: entityIdHint,\n profileKind: kind,\n profileId,\n })\n cache[kind] = loaded ?? null\n const resolvedId = loaded?.entity?.id ?? entityIdHint\n if (resolvedId && typeof resolvedId === 'string') {\n ctx.record.entity_id ??= resolvedId\n ctx.record.entityId ??= resolvedId\n entityIdCache.set(resolvedId, loaded ?? null)\n }\n if (loaded?.entity) {\n if (!ctx.record.entity) ctx.record.entity = loaded.entity\n if (!ctx.record.customer_entity) ctx.record.customer_entity = loaded.entity\n }\n return loaded ?? null\n}\n\nasync function loadCustomerEntityById(ctx: SearchContext, entityId: string | null | undefined): Promise<LoadedCustomerEntity | null> {\n const resolvedId = typeof entityId === 'string' && entityId.length ? entityId : null\n if (!resolvedId) return null\n if (entityIdCache.has(resolvedId)) {\n return entityIdCache.get(resolvedId) ?? null\n }\n const loaded = await loadCustomerEntityBundle(ctx, { entityId: resolvedId })\n entityIdCache.set(resolvedId, loaded ?? null)\n return loaded ?? null\n}\n\nasync function getCustomerEntity(ctx: SearchContext, entityId?: string | null): Promise<Record<string, unknown> | null> {\n const profileCache = profileEntityCache.get(ctx.record)\n if (profileCache) {\n const cached = Object.values(profileCache).find((entry) => {\n if (!entry?.entity) return false\n if (!entityId) return true\n return entry.entity.id === entityId\n })\n if (cached?.entity) return cached.entity\n }\n const inline = getInlineCustomerEntity(ctx.record)\n if (inline && (!entityId || inline.id === entityId)) {\n if (inline.id && typeof inline.id === 'string') {\n entityIdCache.set(inline.id, { entity: inline, customFields: {} })\n }\n return inline\n }\n const resolvedId = entityId ?? resolveCustomerEntityId(ctx.record)\n const loaded = await loadCustomerEntityById(ctx, resolvedId)\n return loaded?.entity ?? null\n}\n\ntype HydratedProfileContext = {\n entity: Record<string, unknown> | null\n entityId: string | null\n profileCustomFields: Record<string, unknown>\n entityCustomFields: Record<string, unknown>\n entityOnlyCustomFields: Record<string, unknown>\n}\n\nasync function hydrateProfileContext(ctx: SearchContext, kind: CustomerProfileKind): Promise<HydratedProfileContext> {\n const profileCustomFields = ctx.customFields ?? {}\n const loaded = await loadCustomerEntityForProfile(ctx, kind)\n let entity = loaded?.entity ?? getInlineCustomerEntity(ctx.record)\n let entityCustomFields = loaded?.customFields ?? {}\n let entityId = (entity?.id as string | undefined) ?? resolveCustomerEntityId(ctx.record)\n if (!entity && entityId) {\n const fetched = await loadCustomerEntityById(ctx, entityId)\n entity = fetched?.entity ?? null\n if (fetched?.customFields) {\n entityCustomFields = Object.keys(entityCustomFields).length ? entityCustomFields : fetched.customFields\n }\n }\n if (!entity && !entityId) {\n entityId = resolveCustomerEntityId(ctx.record)\n }\n if (entity?.id && typeof entity.id === 'string') {\n entityId = entity.id\n ctx.record.entity_id ??= entity.id\n ctx.record.entityId ??= entity.id\n if (!ctx.record.entity) ctx.record.entity = entity\n if (!ctx.record.customer_entity) ctx.record.customer_entity = entity\n }\n const entityOnlyCustomFields = subtractCustomFields(profileCustomFields, entityCustomFields)\n return {\n entity: entity ?? null,\n entityId: entityId ?? null,\n profileCustomFields,\n entityCustomFields,\n entityOnlyCustomFields,\n }\n}\n\nasync function loadRecord(ctx: SearchContext, entityId: string, recordId?: string | null) {\n if (!recordId || !ctx.queryEngine) return null\n const res = await ctx.queryEngine.query(entityId, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? undefined,\n filters: { id: recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n })\n return res.items[0] as Record<string, unknown> | undefined\n}\n\nfunction resolveCustomerEntityId(record: Record<string, unknown>): string | null {\n const direct =\n record.customer_entity_id ??\n record.entityId ??\n record.entity_id ??\n record.customerEntityId ??\n record.customerEntityID ??\n (typeof record.entity === 'object' && record.entity ? (record.entity as Record<string, unknown>).id : undefined) ??\n (typeof record.customer_entity === 'object' && record.customer_entity ? (record.customer_entity as Record<string, unknown>).id : undefined)\n const value = typeof direct === 'string' && direct.length ? direct : null\n return value\n}\n\nfunction getInlineCustomerEntity(record: Record<string, unknown>): Record<string, unknown> | null {\n const inline =\n (typeof record.entity === 'object' && record.entity) ||\n (typeof record.customer_entity === 'object' && record.customer_entity) ||\n null\n return inline as Record<string, unknown> | null\n}\n\nasync function getLinkedTodo(ctx: SearchContext) {\n if (todoCache.has(ctx.record)) {\n return todoCache.get(ctx.record)\n }\n const sourceRaw = typeof ctx.record.todo_source === 'string' ? ctx.record.todo_source : EXAMPLE_TODO_SOURCE\n const [moduleId, entityName] = sourceRaw.split(':')\n const entityId = moduleId && entityName ? `${moduleId}:${entityName}` : CUSTOMER_INTERACTION_TASK_SOURCE\n const todo = await loadRecord(ctx, entityId, ctx.record.todo_id as string ?? ctx.record.todoId as string)\n todoCache.set(ctx.record, todo ?? null)\n return todo ?? null\n}\n\n// =============================================================================\n// URL and Formatting Helpers\n// =============================================================================\n\nfunction buildCustomerUrl(kind: string | null | undefined, id?: string | null): string | null {\n if (!id) return null\n const encoded = encodeURIComponent(id)\n if (kind === 'person') return `/backend/customers/people-v2/${encoded}`\n if (kind === 'company') return `/backend/customers/companies-v2/${encoded}`\n return `/backend/customers/companies-v2/${encoded}`\n}\n\nfunction formatDealValue(record: Record<string, unknown>): string | undefined {\n const amount = record.value_amount ?? record.valueAmount\n if (!amount) return undefined\n const currency = record.value_currency ?? record.valueCurrency ?? ''\n return currency ? `${amount} ${currency}` : String(amount)\n}\n\nfunction snippet(text: unknown, max = 140): string | undefined {\n if (typeof text !== 'string') return undefined\n const trimmed = text.trim()\n if (!trimmed.length) return undefined\n if (trimmed.length <= max) return trimmed\n return `${trimmed.slice(0, max - 3)}...`\n}\n\nfunction appendLine(lines: string[], label: string, value: unknown) {\n if (value === null || value === undefined) return\n const text = Array.isArray(value)\n ? value.map((item) => (item === null || item === undefined ? '' : String(item))).filter(Boolean).join(', ')\n : (typeof value === 'object' ? JSON.stringify(value) : String(value))\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction friendlyLabel(input: string): string {\n return input\n .replace(/^cf:/, '')\n .replace(/_/g, ' ')\n .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a} ${b}`)\n .replace(/\\b\\w/g, (char) => char.toUpperCase())\n}\n\nfunction appendCustomFieldLines(lines: string[], customFields: Record<string, unknown>, prefix: string) {\n for (const [key, value] of Object.entries(customFields)) {\n if (value === null || value === undefined) continue\n const label = prefix ? `${prefix} ${friendlyLabel(key)}` : friendlyLabel(key)\n appendLine(lines, label, value)\n }\n}\n\nfunction pickValue(source: Record<string, unknown> | null | undefined, ...keys: string[]): unknown {\n if (!source) return undefined\n for (const key of keys) {\n if (key in source && source[key] != null) return source[key]\n }\n return undefined\n}\n\nfunction pickString(...candidates: unknown[]): string | null {\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim().length) {\n return candidate.trim()\n }\n }\n return null\n}\n\nfunction pickLabel(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n if (candidate === null || candidate === undefined) continue\n const value = typeof candidate === 'string' ? candidate : String(candidate)\n const trimmed = value.trim()\n if (trimmed.length) return trimmed\n }\n return null\n}\n\nfunction appendCustomerEntityLines(\n lines: string[],\n entity: Record<string, unknown> | null,\n contactLabel: 'Customer' | 'Primary' = 'Customer',\n) {\n if (!entity) return\n appendLine(lines, 'Customer', pickValue(entity, 'display_name', 'displayName') ?? entity.id)\n appendLine(lines, `${contactLabel} email`, pickValue(entity, 'primary_email', 'primaryEmail'))\n appendLine(lines, `${contactLabel} phone`, pickValue(entity, 'primary_phone', 'primaryPhone'))\n appendLine(lines, 'Lifecycle stage', pickValue(entity, 'lifecycle_stage', 'lifecycleStage'))\n appendLine(lines, 'Status', pickValue(entity, 'status'))\n}\n\nfunction ensureFallbackLines(lines: string[], record: Record<string, unknown>, options: { includeId?: boolean } = {}) {\n if (lines.length) return\n const excluded = new Set(['tenant_id', 'organization_id', 'created_at', 'updated_at', 'deleted_at'])\n for (const [key, value] of Object.entries(record)) {\n if (value === null || value === undefined) continue\n if (excluded.has(key)) continue\n if (key === 'id') continue\n appendLine(lines, friendlyLabel(key), value)\n }\n if (!lines.length && options.includeId !== false) {\n const fallbackId =\n record.id ??\n record.entity_id ??\n record.customer_entity_id ??\n record.entityId ??\n record.customerEntityId ??\n null\n if (fallbackId) {\n appendLine(lines, 'Record ID', fallbackId)\n }\n }\n}\n\n// =============================================================================\n// Presenter Functions\n// =============================================================================\n\nfunction resolvePersonPresenter(\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n customFields: Record<string, unknown>,\n): SearchResultPresenter {\n const fallbackEntityId = resolveCustomerEntityId(record)\n const firstName = record.first_name ?? record.firstName ?? customFields.first_name ?? customFields.firstName ?? ''\n const lastName = record.last_name ?? record.lastName ?? customFields.last_name ?? customFields.lastName ?? ''\n const nameParts = [firstName, lastName].filter(Boolean).join(' ')\n const title =\n (pickValue(entity, 'display_name', 'displayName') as string | undefined) ??\n (record.preferred_name as string | undefined) ??\n (record.preferredName as string | undefined) ??\n (nameParts.length ? nameParts : undefined) ??\n fallbackEntityId ??\n (record.id as string | undefined) ??\n 'Person'\n const subtitlePieces: string[] = []\n const jobTitle = record.job_title ?? record.jobTitle ?? customFields.job_title ?? customFields.jobTitle\n if (jobTitle) subtitlePieces.push(String(jobTitle))\n const department = record.department ?? customFields.department\n if (department) subtitlePieces.push(String(department))\n const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')\n if (primaryEmail) subtitlePieces.push(String(primaryEmail))\n const primaryPhone = pickValue(entity, 'primary_phone', 'primaryPhone')\n if (primaryPhone) subtitlePieces.push(String(primaryPhone))\n const summary = snippet(\n (pickValue(entity, 'description') as string | undefined) ??\n (customFields.summary as string | undefined) ??\n (customFields.description as string | undefined),\n )\n if (summary) subtitlePieces.push(summary)\n return {\n title: String(title),\n subtitle: subtitlePieces.length ? subtitlePieces.join(' \u00B7 ') : undefined,\n icon: 'user',\n badge: pickValue(entity, 'display_name', 'displayName') ? 'Person' : undefined,\n }\n}\n\nfunction resolveCompanyPresenter(\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n customFields: Record<string, unknown>,\n): SearchResultPresenter {\n const fallbackEntityId = resolveCustomerEntityId(record)\n const title =\n (pickValue(entity, 'display_name', 'displayName') as string | undefined) ??\n (customFields.display_name as string | undefined) ??\n (customFields.displayName as string | undefined) ??\n (record.brand_name as string | undefined) ??\n (record.legal_name as string | undefined) ??\n (record.domain as string | undefined) ??\n (record.brandName as string | undefined) ??\n (record.legalName as string | undefined) ??\n (entity?.id && entity?.display_name ? entity.display_name as string : undefined) ??\n fallbackEntityId ??\n (record.id as string | undefined) ??\n 'Company'\n const subtitlePieces: string[] = []\n const industry = record.industry\n if (industry) subtitlePieces.push(String(industry))\n const sizeBucket = record.size_bucket ?? record.sizeBucket\n if (sizeBucket) subtitlePieces.push(String(sizeBucket))\n if (entity) {\n const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')\n if (primaryEmail) subtitlePieces.push(String(primaryEmail))\n }\n const summary = snippet(\n (pickValue(entity, 'description') as string | undefined) ??\n (customFields.summary as string | undefined) ??\n (customFields.description as string | undefined) ??\n (record.summary as string | undefined) ??\n (record.description as string | undefined),\n )\n if (summary) subtitlePieces.push(summary)\n if (!entity && (!title || title === fallbackEntityId)) {\n console.warn('[search.customers] Missing customer entity during company presenter build', {\n recordId: record.id ?? null,\n entityId: fallbackEntityId,\n recordKeys: Object.keys(record),\n })\n }\n return {\n title: String(title),\n subtitle: subtitlePieces.length ? subtitlePieces.join(' \u00B7 ') : undefined,\n icon: 'building',\n badge: pickValue(entity, 'display_name', 'displayName') ? 'Company' : undefined,\n }\n}\n\nfunction logMissingPresenterTitle(\n kind: 'person' | 'company',\n record: Record<string, unknown>,\n entity: Record<string, unknown> | null,\n presenter: SearchResultPresenter,\n) {\n const fallbackId = record.id ?? record.entity_id ?? resolveCustomerEntityId(record)\n if (!fallbackId) return\n if (presenter.title && presenter.title !== String(fallbackId)) return\n console.warn('[search.customers] Presenter fell back to record id', {\n kind,\n recordId: fallbackId,\n entityId: resolveCustomerEntityId(record),\n entityDisplayName: entity?.display_name ?? null,\n })\n}\n\n// =============================================================================\n// Search Module Configuration\n// =============================================================================\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n // =========================================================================\n // Person Profile\n // =========================================================================\n {\n entityId: 'customers:customer_person_profile',\n enabled: true,\n priority: 10,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Preferred name', record.preferred_name ?? record.preferredName ?? ctx.customFields.preferred_name)\n appendLine(lines, 'First name', record.first_name ?? record.firstName ?? ctx.customFields.first_name)\n appendLine(lines, 'Last name', record.last_name ?? record.lastName ?? ctx.customFields.last_name)\n appendLine(lines, 'Job title', record.job_title ?? record.jobTitle ?? ctx.customFields.job_title)\n appendLine(lines, 'Department', record.department ?? record.department_name ?? record.departmentName ?? ctx.customFields.department)\n appendLine(lines, 'Seniority', record.seniority ?? record.seniority_level ?? record.seniorityLevel ?? ctx.customFields.seniority)\n appendLine(lines, 'Timezone', record.timezone ?? record.time_zone ?? record.timeZone ?? ctx.customFields.timezone)\n appendLine(lines, 'LinkedIn', record.linked_in_url ?? record.linkedInUrl ?? ctx.customFields.linked_in_url)\n appendLine(lines, 'Twitter', record.twitter_url ?? record.twitterUrl ?? ctx.customFields.twitter_url)\n\n const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =\n await hydrateProfileContext(ctx, 'person')\n appendCustomFieldLines(lines, profileCustomFields, 'Person custom')\n if (Object.keys(entityOnlyCustomFields).length) {\n appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')\n }\n if (!entity) {\n console.warn('[search.customers] Failed to load customer entity for person profile', {\n recordId: record.id,\n entityId,\n recordKeys: Object.keys(record),\n })\n }\n appendCustomerEntityLines(lines, entity, 'Customer')\n ensureFallbackLines(lines, record)\n if (!lines.length) return null\n\n if (!entityId) {\n console.warn('[search.customers] person profile missing entity id', {\n recordId: record.id,\n recordKeys: Object.keys(record),\n })\n }\n\n const presenter = resolvePersonPresenter(record, entity, ctx.customFields)\n logMissingPresenterTitle('person', record, entity, presenter)\n const presenterLabel = pickLabel(presenter.title) ?? 'Open person'\n const links: SearchResultLink[] = []\n if (entityId) {\n const href = buildCustomerUrl('person', entityId)\n if (href) {\n links.push({ href, label: presenterLabel, kind: 'primary' })\n }\n }\n\n return {\n text: lines,\n presenter,\n links,\n checksumSource: {\n record: ctx.record,\n customFields: profileCustomFields,\n entity,\n entityCustomFields,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))\n return resolvePersonPresenter(ctx.record, entity, ctx.customFields)\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n return buildCustomerUrl('person', entityId)\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n if (!entityId) return null\n const href = buildCustomerUrl('person', entityId)\n if (!href) return null\n return [{ href: `${href}/edit`, label: 'Edit', kind: 'secondary' }]\n },\n\n fieldPolicy: {\n searchable: [\n 'preferred_name',\n 'first_name',\n 'last_name',\n 'job_title',\n 'department',\n 'seniority',\n 'timezone',\n 'linked_in_url',\n 'twitter_url',\n ],\n hashOnly: ['primary_email', 'primary_phone', 'personal_email'],\n excluded: ['date_of_birth', 'government_id', 'ssn', 'tax_id'],\n },\n aclFeatures: ['customers.people.view'],\n },\n\n // =========================================================================\n // Company Profile\n // =========================================================================\n {\n entityId: 'customers:customer_company_profile',\n enabled: true,\n priority: 10,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Legal name', record.legal_name ?? record.legalName ?? ctx.customFields.legal_name)\n appendLine(lines, 'Brand name', record.brand_name ?? record.brandName ?? ctx.customFields.brand_name)\n appendLine(lines, 'Domain', record.domain ?? record.website_domain ?? record.websiteDomain ?? ctx.customFields.domain)\n appendLine(lines, 'Website', record.website_url ?? record.websiteUrl ?? ctx.customFields.website_url)\n appendLine(lines, 'Industry', record.industry ?? ctx.customFields.industry)\n appendLine(lines, 'Company size', record.size_bucket ?? record.sizeBucket ?? ctx.customFields.size_bucket)\n appendLine(lines, 'Annual revenue', record.annual_revenue ?? record.annualRevenue ?? ctx.customFields.annual_revenue)\n\n const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =\n await hydrateProfileContext(ctx, 'company')\n appendCustomFieldLines(lines, profileCustomFields, 'Company custom')\n if (Object.keys(entityOnlyCustomFields).length) {\n appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')\n }\n appendCustomerEntityLines(lines, entity, 'Primary')\n ensureFallbackLines(lines, record)\n if (!lines.length) return null\n\n const presenter = resolveCompanyPresenter(record, entity, ctx.customFields)\n logMissingPresenterTitle('company', record, entity, presenter)\n const primaryLabel = pickLabel(presenter.title) ?? 'Open company'\n const links: SearchResultLink[] = []\n if (entityId) {\n const href = buildCustomerUrl('company', entityId)\n if (href) {\n links.push({ href, label: primaryLabel, kind: 'primary' })\n }\n }\n\n return {\n text: lines,\n presenter,\n links,\n checksumSource: {\n record: ctx.record,\n customFields: profileCustomFields,\n entity,\n entityCustomFields,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const entity = await getCustomerEntity(ctx, resolveCustomerEntityId(ctx.record))\n return resolveCompanyPresenter(ctx.record, entity, ctx.customFields)\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n return buildCustomerUrl('company', entityId)\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const entityId = resolveCustomerEntityId(ctx.record)\n if (!entityId) return null\n const href = buildCustomerUrl('company', entityId)\n if (!href) return null\n return [{ href: `${href}/edit`, label: 'Edit', kind: 'secondary' }]\n },\n\n fieldPolicy: {\n searchable: [\n 'legal_name',\n 'brand_name',\n 'display_name',\n 'domain',\n 'website_url',\n 'industry',\n 'size_bucket',\n 'description',\n ],\n hashOnly: ['tax_id', 'registration_number'],\n excluded: ['bank_account', 'billing_info', 'credit_info'],\n },\n aclFeatures: ['customers.companies.view'],\n },\n\n // =========================================================================\n // Customer Comment\n // =========================================================================\n {\n entityId: 'customers:customer_comment',\n enabled: true,\n priority: 6,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n lines.push(`Note: ${ctx.record.body ?? ''}`)\n if (ctx.record.appearance_icon) lines.push(`Icon: ${ctx.record.appearance_icon}`)\n if (ctx.record.appearance_color) lines.push(`Color: ${ctx.record.appearance_color}`)\n\n const presenter: SearchResultPresenter | undefined = parent?.display_name\n ? {\n title: parent.display_name as string,\n subtitle: snippet(ctx.record.body),\n icon: parent.kind === 'person' ? 'user' : 'building',\n }\n : undefined\n\n return {\n text: lines,\n presenter,\n checksumSource: {\n body: ctx.record.body,\n entityId: ctx.record.entity_id ?? null,\n updatedAt: ctx.record.updated_at ?? ctx.record.updatedAt ?? null,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const title = (parent?.display_name as string | undefined) ?? 'Customer note'\n return {\n title,\n subtitle: snippet(ctx.record.body),\n icon: 'sticky-note',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#notes` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n assertTenantContext(ctx)\n const links: SearchResultLink[] = []\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const parentUrl = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n if (parentUrl) {\n links.push({ href: parentUrl, label: (parent?.display_name as string | undefined) ?? 'View customer', kind: 'primary' })\n }\n if (ctx.record.deal_id) {\n const dealUrl = `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`\n links.push({ href: dealUrl, label: 'Open deal', kind: 'secondary' })\n }\n return links.length ? links : null\n },\n\n fieldPolicy: {\n searchable: ['body'],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n\n // =========================================================================\n // Customer Deal\n // =========================================================================\n {\n entityId: 'customers:customer_deal',\n enabled: true,\n priority: 8,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n const lines: string[] = []\n const record = ctx.record\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Stage', record.pipeline_stage)\n appendLine(lines, 'Status', record.status)\n appendLine(lines, 'Source', record.source)\n const value = formatDealValue(record)\n if (value) appendLine(lines, 'Value', value)\n if (!lines.length) return null\n\n const subtitleParts: string[] = []\n if (record.pipeline_stage) subtitleParts.push(String(record.pipeline_stage))\n if (record.status) subtitleParts.push(String(record.status))\n if (value) subtitleParts.push(value)\n\n return {\n text: lines,\n presenter: {\n title: String(record.title ?? 'Deal'),\n subtitle: subtitleParts.join(' \u00B7 ') || undefined,\n icon: 'briefcase',\n badge: 'Deal',\n },\n checksumSource: {\n title: record.title,\n status: record.status,\n stage: record.pipeline_stage,\n value: value,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n const { record } = ctx\n const title = pickString(record.title as string, 'Deal')\n const subtitleParts: string[] = []\n if (record.pipeline_stage) subtitleParts.push(String(record.pipeline_stage))\n if (record.status) subtitleParts.push(String(record.status))\n const amount = record.value_amount ?? record.valueAmount\n const currency = record.value_currency ?? record.valueCurrency\n if (amount) {\n subtitleParts.push(currency ? `${amount} ${currency}` : String(amount))\n }\n\n return {\n title: title ?? 'Deal',\n subtitle: subtitleParts.length ? subtitleParts.join(' \u00B7 ') : undefined,\n icon: 'briefcase',\n badge: 'Deal',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n const id = ctx.record.id\n if (!id) return null\n return `/backend/customers/deals/${encodeURIComponent(String(id))}`\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const id = ctx.record.id\n if (!id) return null\n return [\n {\n href: `/backend/customers/deals/${encodeURIComponent(String(id))}/edit`,\n label: 'Edit',\n kind: 'secondary',\n },\n ]\n },\n\n fieldPolicy: {\n searchable: ['title', 'description', 'pipeline_stage', 'status', 'source'],\n hashOnly: [],\n excluded: ['value_amount', 'value_currency'],\n },\n aclFeatures: ['customers.deals.view'],\n },\n\n // =========================================================================\n // Customer Activity\n // =========================================================================\n {\n entityId: 'customers:customer_activity',\n enabled: true,\n priority: 5,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n if (ctx.record.activity_type) lines.push(`Type: ${ctx.record.activity_type}`)\n if (ctx.record.subject) lines.push(`Subject: ${ctx.record.subject}`)\n if (ctx.record.body) lines.push(`Body: ${ctx.record.body}`)\n\n const presenter: SearchResultPresenter = {\n title: ctx.record.subject ? String(ctx.record.subject) : `Activity: ${ctx.record.activity_type ?? 'update'}`,\n subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),\n icon: 'bolt',\n }\n\n return {\n text: lines,\n presenter,\n checksumSource: {\n subject: ctx.record.subject,\n body: ctx.record.body,\n entityId: ctx.record.entity_id ?? null,\n updatedAt: ctx.record.updated_at ?? ctx.record.updatedAt ?? null,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n return {\n title: ctx.record.subject ? String(ctx.record.subject) : `Activity: ${ctx.record.activity_type ?? 'update'}`,\n subtitle: (parent?.display_name as string | undefined) ?? snippet(ctx.record.body),\n icon: 'bolt',\n badge: 'Activity',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#activity-${ctx.record.id ?? ctx.record.activity_id ?? ''}` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const links: SearchResultLink[] = []\n if (ctx.record.deal_id) {\n links.push({\n href: `/backend/customers/deals/${encodeURIComponent(ctx.record.deal_id as string)}`,\n label: 'Open deal',\n kind: 'secondary',\n })\n }\n return links.length ? links : null\n },\n\n fieldPolicy: {\n searchable: ['subject', 'body', 'activity_type'],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n\n // =========================================================================\n // Customer Todo Link\n // =========================================================================\n {\n entityId: 'customers:customer_todo_link',\n enabled: true,\n priority: 4,\n\n buildSource: async (ctx: SearchBuildContext): Promise<SearchIndexSource | null> => {\n assertTenantContext(ctx)\n const todo = await getLinkedTodo(ctx) as Record<string, unknown> | null\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const lines: string[] = []\n if (todo?.title) lines.push(`Todo: ${todo.title}`)\n if (todo?.is_done !== undefined) lines.push(`Status: ${todo.is_done ? 'Done' : 'Open'}`)\n if (parent?.display_name) lines.push(`Customer: ${parent.display_name}`)\n if (!lines.length) return null\n\n return {\n text: lines,\n presenter: todo?.title\n ? { title: todo.title as string, subtitle: parent?.display_name as string | undefined, icon: 'check-square' }\n : undefined,\n checksumSource: {\n todoId: ctx.record.todo_id ?? ctx.record.todoId,\n todoSource: ctx.record.todo_source ?? ctx.record.todoSource,\n entityId: ctx.record.entity_id ?? ctx.record.entityId,\n },\n }\n },\n\n formatResult: async (ctx: SearchBuildContext): Promise<SearchResultPresenter | null> => {\n assertTenantContext(ctx)\n const todo = await getLinkedTodo(ctx) as Record<string, unknown> | null\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n return {\n title: (todo?.title as string | undefined) ?? 'Customer task',\n subtitle: parent?.display_name as string | undefined,\n icon: 'check-square',\n }\n },\n\n resolveUrl: async (ctx: SearchBuildContext): Promise<string | null> => {\n assertTenantContext(ctx)\n const parent = await getCustomerEntity(ctx, ctx.record.entity_id as string ?? ctx.record.entityId as string)\n const base = buildCustomerUrl(parent?.kind as string ?? null, (parent?.id ?? ctx.record.entity_id ?? ctx.record.entityId) as string)\n return base ? `${base}#tasks` : null\n },\n\n resolveLinks: async (ctx: SearchBuildContext): Promise<SearchResultLink[] | null> => {\n const todoId = ctx.record.todo_id ?? ctx.record.todoId\n if (!todoId) return null\n return [{\n href: `/backend/todos/${encodeURIComponent(todoId as string)}/edit`,\n label: 'Open todo',\n kind: 'secondary',\n }]\n },\n\n fieldPolicy: {\n searchable: [],\n hashOnly: [],\n excluded: [],\n },\n aclFeatures: ['customers.activities.view'],\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.5-develop.5187.1.82e5532561",
3
+ "version": "0.6.5-develop.5200.1.871eca3402",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -245,16 +245,16 @@
245
245
  "zod": "^4.4.3"
246
246
  },
247
247
  "peerDependencies": {
248
- "@open-mercato/ai-assistant": "0.6.5-develop.5187.1.82e5532561",
249
- "@open-mercato/shared": "0.6.5-develop.5187.1.82e5532561",
250
- "@open-mercato/ui": "0.6.5-develop.5187.1.82e5532561",
248
+ "@open-mercato/ai-assistant": "0.6.5-develop.5200.1.871eca3402",
249
+ "@open-mercato/shared": "0.6.5-develop.5200.1.871eca3402",
250
+ "@open-mercato/ui": "0.6.5-develop.5200.1.871eca3402",
251
251
  "react": "^19.0.0",
252
252
  "react-dom": "^19.0.0"
253
253
  },
254
254
  "devDependencies": {
255
- "@open-mercato/ai-assistant": "0.6.5-develop.5187.1.82e5532561",
256
- "@open-mercato/shared": "0.6.5-develop.5187.1.82e5532561",
257
- "@open-mercato/ui": "0.6.5-develop.5187.1.82e5532561",
255
+ "@open-mercato/ai-assistant": "0.6.5-develop.5200.1.871eca3402",
256
+ "@open-mercato/shared": "0.6.5-develop.5200.1.871eca3402",
257
+ "@open-mercato/ui": "0.6.5-develop.5200.1.871eca3402",
258
258
  "@testing-library/dom": "^10.4.1",
259
259
  "@testing-library/jest-dom": "^6.9.1",
260
260
  "@testing-library/react": "^16.3.1",
@@ -1,5 +1,5 @@
1
1
  import { promises as fs } from 'fs'
2
- import path from 'path'
2
+ import { resolveContainedPath, resolveLegacyPublicRoot } from '../pathContainment'
3
3
  import type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'
4
4
 
5
5
  export class LegacyPublicStorageDriver implements StorageDriver {
@@ -35,12 +35,6 @@ export class LegacyPublicStorageDriver implements StorageDriver {
35
35
  }
36
36
 
37
37
  private resolveAbsolutePath(storagePath: string): string {
38
- let safeRelative = storagePath.replace(/^\/*/, '')
39
- let prev: string
40
- do {
41
- prev = safeRelative
42
- safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, '')
43
- } while (safeRelative !== prev)
44
- return path.join(process.cwd(), safeRelative)
38
+ return resolveContainedPath(process.cwd(), storagePath, resolveLegacyPublicRoot())
45
39
  }
46
40
  }
@@ -2,6 +2,7 @@ import { promises as fs } from 'fs'
2
2
  import path from 'path'
3
3
  import { randomUUID } from 'crypto'
4
4
  import { resolvePartitionRoot } from '../storage'
5
+ import { resolveContainedPath } from '../pathContainment'
5
6
  import type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'
6
7
 
7
8
  function sanitizeFileName(fileName: string): string {
@@ -67,13 +68,7 @@ export class LocalStorageDriver implements StorageDriver {
67
68
  }
68
69
 
69
70
  private resolveAbsolutePath(partitionCode: string, storagePath: string): string {
70
- let safeRelative = storagePath.replace(/^\/*/, '')
71
- let prev: string
72
- do {
73
- prev = safeRelative
74
- safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, '')
75
- } while (safeRelative !== prev)
76
71
  const root = resolvePartitionRoot(partitionCode)
77
- return path.join(root, safeRelative)
72
+ return resolveContainedPath(root, storagePath)
78
73
  }
79
74
  }
@@ -0,0 +1,42 @@
1
+ import path from 'path'
2
+
3
+ /**
4
+ * Reduce a stored relative path to safe segments: drop leading slashes, empty
5
+ * segments, `.`, and `..`. The result can never contain a traversal segment,
6
+ * so joining it onto a root can never climb above that root.
7
+ */
8
+ export function sanitizeStorageRelativePath(storagePath: string): string {
9
+ return String(storagePath ?? '')
10
+ .split(/[\\/]+/)
11
+ .filter((segment) => segment.length > 0 && segment !== '.' && segment !== '..')
12
+ .join(path.sep)
13
+ }
14
+
15
+ /**
16
+ * Resolve a stored path against `joinRoot` and assert the result stays within
17
+ * `containmentRoot` (defaults to `joinRoot`). Throws when the resolved path
18
+ * escapes the boundary — e.g. a legacy row whose path points outside `public/`.
19
+ */
20
+ export function resolveContainedPath(
21
+ joinRoot: string,
22
+ storagePath: string,
23
+ containmentRoot?: string,
24
+ ): string {
25
+ const base = path.resolve(joinRoot)
26
+ const boundary = path.resolve(containmentRoot ?? joinRoot)
27
+ const candidate = path.resolve(base, sanitizeStorageRelativePath(storagePath))
28
+ const relativeToBoundary = path.relative(boundary, candidate)
29
+ if (relativeToBoundary.startsWith('..') || path.isAbsolute(relativeToBoundary)) {
30
+ throw new Error('[internal] attachment storage path escapes its containment root')
31
+ }
32
+ return candidate
33
+ }
34
+
35
+ /**
36
+ * The fixed sub-root that `legacyPublic` rows are allowed to resolve within.
37
+ * Stored paths include the `public/` prefix (see Migration20251117181353), so
38
+ * they are joined onto `process.cwd()` but constrained to `process.cwd()/public`.
39
+ */
40
+ export function resolveLegacyPublicRoot(): string {
41
+ return path.join(process.cwd(), 'public')
42
+ }
@@ -2,6 +2,7 @@ import { promises as fs } from 'fs'
2
2
  import path from 'path'
3
3
  import { randomUUID } from 'crypto'
4
4
  import { resolvePartitionEnvKey } from './partitionEnv'
5
+ import { resolveContainedPath, resolveLegacyPublicRoot } from './pathContainment'
5
6
 
6
7
  export function resolvePartitionRoot(code: string): string {
7
8
  const envKey = resolvePartitionEnvKey(code)
@@ -74,18 +75,11 @@ export function resolveAttachmentAbsolutePath(
74
75
  storagePath: string,
75
76
  storageDriver?: string | null
76
77
  ): string {
77
- // Remove leading slashes first
78
- let safeRelative = storagePath.replace(/^\/*/, '')
79
- // Remove all ../ (and ..\) path traversal segments, repeatedly until gone
80
- do {
81
- var prev = safeRelative
82
- safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, '')
83
- } while (safeRelative !== prev)
84
78
  if (storageDriver === 'legacyPublic') {
85
- return path.join(process.cwd(), safeRelative)
79
+ return resolveContainedPath(process.cwd(), storagePath, resolveLegacyPublicRoot())
86
80
  }
87
81
  const root = resolvePartitionRoot(partitionCode)
88
- return path.join(root, safeRelative)
82
+ return resolveContainedPath(root, storagePath)
89
83
  }
90
84
 
91
85
  /**
@@ -363,9 +363,9 @@ async function getLinkedTodo(ctx: SearchContext) {
363
363
  function buildCustomerUrl(kind: string | null | undefined, id?: string | null): string | null {
364
364
  if (!id) return null
365
365
  const encoded = encodeURIComponent(id)
366
- if (kind === 'person') return `/backend/customers/people/${encoded}`
367
- if (kind === 'company') return `/backend/customers/companies/${encoded}`
368
- return `/backend/customers/companies/${encoded}`
366
+ if (kind === 'person') return `/backend/customers/people-v2/${encoded}`
367
+ if (kind === 'company') return `/backend/customers/companies-v2/${encoded}`
368
+ return `/backend/customers/companies-v2/${encoded}`
369
369
  }
370
370
 
371
371
  function formatDealValue(record: Record<string, unknown>): string | undefined {