@open-mercato/shared 0.6.7-develop.6860.1.f796b22e80 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +6 -57
  3. package/build.mjs +4 -2
  4. package/dist/lib/auth/server.js +2 -15
  5. package/dist/lib/auth/server.js.map +2 -2
  6. package/dist/lib/bootstrap/dynamicLoader.js +43 -117
  7. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  8. package/dist/lib/commands/command-bus.js +1 -5
  9. package/dist/lib/commands/command-bus.js.map +2 -2
  10. package/dist/lib/commands/command-interceptor-runner.js +2 -2
  11. package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
  12. package/dist/lib/crud/enricher-runner.js +11 -2
  13. package/dist/lib/crud/enricher-runner.js.map +2 -2
  14. package/dist/lib/crud/factory.js +4 -63
  15. package/dist/lib/crud/factory.js.map +2 -2
  16. package/dist/lib/crud/interceptor-runner.js +2 -2
  17. package/dist/lib/crud/interceptor-runner.js.map +2 -2
  18. package/dist/lib/crud/mutation-guard-registry.js +2 -2
  19. package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
  20. package/dist/lib/crud/types.js +0 -4
  21. package/dist/lib/crud/types.js.map +3 -3
  22. package/dist/lib/data/engine.js +11 -37
  23. package/dist/lib/data/engine.js.map +2 -2
  24. package/dist/lib/db/mikro.js +12 -3
  25. package/dist/lib/db/mikro.js.map +2 -2
  26. package/dist/lib/db/pg-errors.js +0 -42
  27. package/dist/lib/db/pg-errors.js.map +2 -2
  28. package/dist/lib/di/container.js +6 -48
  29. package/dist/lib/di/container.js.map +2 -2
  30. package/dist/lib/email/send.js +0 -56
  31. package/dist/lib/email/send.js.map +2 -2
  32. package/dist/lib/encryption/subscriber.js +10 -2
  33. package/dist/lib/encryption/subscriber.js.map +2 -2
  34. package/dist/lib/encryption/tenantDataEncryptionService.js +17 -52
  35. package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
  36. package/dist/lib/i18n/config.js +1 -1
  37. package/dist/lib/i18n/config.js.map +2 -2
  38. package/dist/lib/i18n/server.js +4 -4
  39. package/dist/lib/i18n/server.js.map +3 -3
  40. package/dist/lib/logger/index.js +3 -47
  41. package/dist/lib/logger/index.js.map +3 -3
  42. package/dist/lib/modules/registry.js +1 -5
  43. package/dist/lib/modules/registry.js.map +2 -2
  44. package/dist/lib/query/engine.js +40 -54
  45. package/dist/lib/query/engine.js.map +2 -2
  46. package/dist/lib/query/types.js.map +1 -1
  47. package/dist/lib/redis/connection.js +2 -7
  48. package/dist/lib/redis/connection.js.map +2 -2
  49. package/dist/lib/search/config.js +2 -61
  50. package/dist/lib/search/config.js.map +2 -2
  51. package/dist/lib/search/tokenize.js +11 -23
  52. package/dist/lib/search/tokenize.js.map +2 -2
  53. package/dist/lib/version.js +1 -1
  54. package/dist/lib/version.js.map +1 -1
  55. package/dist/lib/webhooks/index.js +1 -13
  56. package/dist/lib/webhooks/index.js.map +2 -2
  57. package/dist/lib/webhooks/verify.js +7 -12
  58. package/dist/lib/webhooks/verify.js.map +2 -2
  59. package/dist/modules/analytics.js.map +2 -2
  60. package/dist/modules/overrides.js +1 -50
  61. package/dist/modules/overrides.js.map +2 -2
  62. package/dist/modules/payment_gateways/types.js +1 -8
  63. package/dist/modules/payment_gateways/types.js.map +2 -2
  64. package/dist/modules/registry.js +0 -1
  65. package/dist/modules/registry.js.map +2 -2
  66. package/dist/security/enabledModulesRegistry.js +11 -56
  67. package/dist/security/enabledModulesRegistry.js.map +2 -2
  68. package/jest.config.cjs +1 -2
  69. package/package.json +9 -19
  70. package/src/lib/ai/llm-provider.ts +0 -33
  71. package/src/lib/auth/__tests__/server.test.ts +0 -23
  72. package/src/lib/auth/server.ts +2 -23
  73. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +8 -63
  74. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +1 -21
  75. package/src/lib/bootstrap/dynamicLoader.ts +65 -213
  76. package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +0 -28
  77. package/src/lib/commands/command-bus.ts +1 -5
  78. package/src/lib/commands/command-interceptor-runner.ts +2 -2
  79. package/src/lib/crud/__tests__/crud-factory.test.ts +0 -298
  80. package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +0 -26
  81. package/src/lib/crud/enricher-runner.ts +11 -2
  82. package/src/lib/crud/factory.ts +1 -69
  83. package/src/lib/crud/interceptor-runner.ts +2 -2
  84. package/src/lib/crud/mutation-guard-registry.ts +2 -2
  85. package/src/lib/crud/types.ts +0 -3
  86. package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +3 -18
  87. package/src/lib/data/engine.ts +16 -50
  88. package/src/lib/db/__tests__/mikro.test.ts +25 -0
  89. package/src/lib/db/mikro.ts +38 -13
  90. package/src/lib/db/pg-errors.ts +0 -58
  91. package/src/lib/di/container.ts +8 -72
  92. package/src/lib/email/__tests__/send.test.ts +1 -37
  93. package/src/lib/email/send.ts +0 -79
  94. package/src/lib/encryption/__tests__/subscriber.test.ts +2 -54
  95. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -58
  96. package/src/lib/encryption/subscriber.ts +14 -2
  97. package/src/lib/encryption/tenantDataEncryptionService.ts +18 -64
  98. package/src/lib/i18n/config.ts +2 -2
  99. package/src/lib/i18n/server.ts +2 -6
  100. package/src/lib/logger/__tests__/logger.test.ts +0 -77
  101. package/src/lib/logger/index.ts +1 -59
  102. package/src/lib/modules/registry.ts +0 -10
  103. package/src/lib/query/__tests__/engine.test.ts +13 -65
  104. package/src/lib/query/engine.ts +58 -75
  105. package/src/lib/query/types.ts +5 -4
  106. package/src/lib/redis/__tests__/connection.test.ts +0 -16
  107. package/src/lib/redis/connection.ts +7 -10
  108. package/src/lib/search/__tests__/config.test.ts +0 -177
  109. package/src/lib/search/config.ts +8 -106
  110. package/src/lib/search/tokenize.ts +11 -33
  111. package/src/lib/webhooks/__tests__/verify.test.ts +1 -20
  112. package/src/lib/webhooks/index.ts +1 -19
  113. package/src/lib/webhooks/verify.ts +9 -17
  114. package/src/modules/__tests__/registry.test.ts +2 -10
  115. package/src/modules/analytics.ts +0 -7
  116. package/src/modules/customer-auth.ts +0 -1
  117. package/src/modules/encryption.ts +0 -3
  118. package/src/modules/navigation/backendChrome.ts +0 -15
  119. package/src/modules/overrides.ts +0 -103
  120. package/src/modules/payment_gateways/types.ts +0 -41
  121. package/src/modules/registry.ts +0 -3
  122. package/src/security/enabledModulesRegistry.ts +12 -64
  123. package/dist/lib/ai/safety-identifier.js +0 -32
  124. package/dist/lib/ai/safety-identifier.js.map +0 -7
  125. package/dist/lib/auth/organizationScope.js +0 -34
  126. package/dist/lib/auth/organizationScope.js.map +0 -7
  127. package/dist/lib/bootstrap/clientOnlyModules.js +0 -55
  128. package/dist/lib/bootstrap/clientOnlyModules.js.map +0 -7
  129. package/dist/lib/data/consistency.js +0 -19
  130. package/dist/lib/data/consistency.js.map +0 -7
  131. package/dist/lib/db/entityMetadata.js +0 -26
  132. package/dist/lib/db/entityMetadata.js.map +0 -7
  133. package/dist/lib/logger/extension.js +0 -29
  134. package/dist/lib/logger/extension.js.map +0 -7
  135. package/dist/lib/modules/surfaceFingerprint.js +0 -47
  136. package/dist/lib/modules/surfaceFingerprint.js.map +0 -7
  137. package/dist/lib/query/ciphertext-search-warning.js +0 -45
  138. package/dist/lib/query/ciphertext-search-warning.js.map +0 -7
  139. package/dist/lib/search/auto-indexing.js +0 -14
  140. package/dist/lib/search/auto-indexing.js.map +0 -7
  141. package/dist/lib/search/availability.js +0 -107
  142. package/dist/lib/search/availability.js.map +0 -7
  143. package/dist/lib/search/tokenLookup.js +0 -46
  144. package/dist/lib/search/tokenLookup.js.map +0 -7
  145. package/dist/lib/telemetry/runtime.js +0 -35
  146. package/dist/lib/telemetry/runtime.js.map +0 -7
  147. package/dist/lib/webhooks/body.js +0 -57
  148. package/dist/lib/webhooks/body.js.map +0 -7
  149. package/dist/lib/webhooks/inbound-types.js +0 -1
  150. package/dist/lib/webhooks/inbound-types.js.map +0 -7
  151. package/dist/modules/widgets/extension-points.js +0 -96
  152. package/dist/modules/widgets/extension-points.js.map +0 -7
  153. package/dist/security/featurePolicy.js +0 -62
  154. package/dist/security/featurePolicy.js.map +0 -7
  155. package/scripts/versionSource.cjs +0 -65
  156. package/src/lib/__tests__/versionSource.test.ts +0 -46
  157. package/src/lib/ai/__tests__/llm-provider-contract.test.ts +0 -59
  158. package/src/lib/ai/__tests__/safety-identifier.test.ts +0 -71
  159. package/src/lib/ai/safety-identifier.ts +0 -73
  160. package/src/lib/auth/__tests__/organizationScope.test.ts +0 -107
  161. package/src/lib/auth/organizationScope.ts +0 -65
  162. package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +0 -189
  163. package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +0 -98
  164. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +0 -211
  165. package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +0 -192
  166. package/src/lib/bootstrap/clientOnlyModules.ts +0 -85
  167. package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +0 -171
  168. package/src/lib/data/__tests__/consistency.test.ts +0 -38
  169. package/src/lib/data/consistency.ts +0 -17
  170. package/src/lib/db/__tests__/entityMetadata.test.ts +0 -38
  171. package/src/lib/db/__tests__/pg-errors.test.ts +0 -48
  172. package/src/lib/db/entityMetadata.ts +0 -43
  173. package/src/lib/di/__tests__/container-app-di-absent.test.ts +0 -155
  174. package/src/lib/di/__tests__/container-app-di.test.ts +0 -173
  175. package/src/lib/di/__tests__/registrar-error-log.test.ts +0 -154
  176. package/src/lib/i18n/__tests__/config.test.ts +0 -8
  177. package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +0 -30
  178. package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +0 -29
  179. package/src/lib/logger/extension.ts +0 -59
  180. package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +0 -122
  181. package/src/lib/modules/surfaceFingerprint.ts +0 -87
  182. package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +0 -178
  183. package/src/lib/query/ciphertext-search-warning.ts +0 -95
  184. package/src/lib/search/__tests__/availability.test.ts +0 -211
  185. package/src/lib/search/__tests__/tokenLookup.test.ts +0 -206
  186. package/src/lib/search/__tests__/tokenize.test.ts +0 -49
  187. package/src/lib/search/auto-indexing.ts +0 -22
  188. package/src/lib/search/availability.ts +0 -232
  189. package/src/lib/search/tokenLookup.ts +0 -133
  190. package/src/lib/telemetry/runtime.ts +0 -72
  191. package/src/lib/webhooks/__tests__/body.test.ts +0 -95
  192. package/src/lib/webhooks/body.ts +0 -70
  193. package/src/lib/webhooks/inbound-types.ts +0 -125
  194. package/src/modules/__tests__/nav-group-order-override.test.ts +0 -183
  195. package/src/modules/payment_gateways/__tests__/types.test.ts +0 -37
  196. package/src/modules/widgets/__tests__/extension-points.test.ts +0 -101
  197. package/src/modules/widgets/extension-points.ts +0 -524
  198. package/src/security/__tests__/featurePolicy.test.ts +0 -166
  199. package/src/security/featurePolicy.ts +0 -89
@@ -1,34 +0,0 @@
1
- function normalizeId(value) {
2
- if (typeof value !== "string") return null;
3
- const trimmed = value.trim();
4
- return trimmed.length > 0 ? trimmed : null;
5
- }
6
- function resolveActiveOrganizationId(auth) {
7
- if (!auth) return null;
8
- const selected = normalizeId(auth.orgId);
9
- if (selected) return selected;
10
- const actorOrgId = normalizeId(auth.actorOrgId);
11
- if (!actorOrgId) return null;
12
- if ("actorTenantId" in auth) {
13
- const actorTenantId = normalizeId(auth.actorTenantId);
14
- const effectiveTenantId = normalizeId(auth.tenantId);
15
- if (!actorTenantId || actorTenantId !== effectiveTenantId) return null;
16
- }
17
- return actorOrgId;
18
- }
19
- const ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE = "organization_scope_required";
20
- function organizationScopeRequiredResponse() {
21
- return Response.json(
22
- {
23
- error: "Select an organization to access this resource",
24
- code: ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE
25
- },
26
- { status: 400 }
27
- );
28
- }
29
- export {
30
- ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,
31
- organizationScopeRequiredResponse,
32
- resolveActiveOrganizationId
33
- };
34
- //# sourceMappingURL=organizationScope.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/auth/organizationScope.ts"],
4
- "sourcesContent": ["type OrganizationScopedAuth = {\n orgId?: string | null\n actorOrgId?: unknown\n tenantId?: string | null\n actorTenantId?: unknown\n} | null | undefined\n\nfunction normalizeId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\n/**\n * Resolves the organization a request is scoped to when the caller may be viewing\n * \"all organizations\".\n *\n * Organization-scoped configuration modules (integrations credentials/state, data sync\n * mappings/schedules/runs) all require a non-null `organization_id`, so there is no\n * meaningful \"all organizations\" view of them. When an operator selects that option the\n * super-admin cookie override clears `auth.orgId` and preserves the actor's own\n * organization in `actorOrgId`; fall back to it so those modules keep showing the\n * operator's own configuration instead of failing.\n *\n * The fallback is only valid while the effective tenant is still the actor's own tenant.\n * When the super-admin cookie override also switched tenants (`actorTenantId` is present\n * and differs from `auth.tenantId`), the actor's organization belongs to another tenant \u2014\n * scoping to it would persist a cross-tenant `{ organizationId, tenantId }` pair. Return\n * `null` instead and let the route answer with `organizationScopeRequiredResponse()`.\n *\n * Answering 401 for an unresolvable scope is not merely wrong but self-perpetuating:\n * `apiFetch` reads 401 as an expired session and redirects through\n * `/api/auth/session/refresh`, which succeeds and returns to the same page, reloading\n * forever. That is why the missing-scope answer is a 400, never a 401.\n */\nexport function resolveActiveOrganizationId(auth: OrganizationScopedAuth): string | null {\n if (!auth) return null\n const selected = normalizeId(auth.orgId)\n if (selected) return selected\n const actorOrgId = normalizeId(auth.actorOrgId)\n if (!actorOrgId) return null\n if ('actorTenantId' in auth) {\n const actorTenantId = normalizeId(auth.actorTenantId)\n const effectiveTenantId = normalizeId(auth.tenantId)\n if (!actorTenantId || actorTenantId !== effectiveTenantId) return null\n }\n return actorOrgId\n}\n\nexport const ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE = 'organization_scope_required'\n\n/**\n * 400 response for an authenticated caller whose organization scope cannot be resolved\n * (e.g. a super-admin viewing a foreign tenant with \"all organizations\" selected).\n * Deliberately not a 401: the session is valid, so refreshing it would loop.\n */\nexport function organizationScopeRequiredResponse(): Response {\n return Response.json(\n {\n error: 'Select an organization to access this resource',\n code: ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,\n },\n { status: 400 },\n )\n}\n"],
5
- "mappings": "AAOA,SAAS,YAAY,OAA+B;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAwBO,SAAS,4BAA4B,MAA6C;AACvF,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,WAAW,YAAY,KAAK,KAAK;AACvC,MAAI,SAAU,QAAO;AACrB,QAAM,aAAa,YAAY,KAAK,UAAU;AAC9C,MAAI,CAAC,WAAY,QAAO;AACxB,MAAI,mBAAmB,MAAM;AAC3B,UAAM,gBAAgB,YAAY,KAAK,aAAa;AACpD,UAAM,oBAAoB,YAAY,KAAK,QAAQ;AACnD,QAAI,CAAC,iBAAiB,kBAAkB,kBAAmB,QAAO;AAAA,EACpE;AACA,SAAO;AACT;AAEO,MAAM,yCAAyC;AAO/C,SAAS,oCAA8C;AAC5D,SAAO,SAAS;AAAA,IACd;AAAA,MACE,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,IACA,EAAE,QAAQ,IAAI;AAAA,EAChB;AACF;",
6
- "names": []
7
- }
@@ -1,55 +0,0 @@
1
- const CLIENT_ONLY_STUB_NAMESPACE = "om-client-only-stub";
2
- const LOCAL_IMPORT_PATTERN = /^(\.{1,2}\/|@\/)/;
3
- const CLIENT_ONLY_SUFFIX_PATTERN = /(^|[\\/])[^\\/]+\.client(\.[cm]?[jt]sx?)?$/;
4
- function isClientOnlyModulePath(importPath) {
5
- if (!LOCAL_IMPORT_PATTERN.test(importPath)) return false;
6
- return CLIENT_ONLY_SUFFIX_PATTERN.test(importPath);
7
- }
8
- function encodeJsStringLiteral(value) {
9
- let encoded = "";
10
- for (let index = 0; index < value.length; index += 1) {
11
- const char = value[index];
12
- if (char === '"' || char === "\\") {
13
- encoded += `\\${char}`;
14
- continue;
15
- }
16
- const code = value.charCodeAt(index);
17
- if (code < 32 || code > 126) {
18
- encoded += `\\u${code.toString(16).padStart(4, "0")}`;
19
- continue;
20
- }
21
- encoded += char;
22
- }
23
- return `"${encoded}"`;
24
- }
25
- function renderClientOnlyModuleStub(importPath) {
26
- const message = `[internal] Client-only module ${importPath} is not available in the CLI runtime. It is excluded from the CLI bundle because it renders browser components.`;
27
- return [
28
- `function clientOnlyModuleUnavailable() { throw new Error(${encodeJsStringLiteral(message)}) }`,
29
- "export default clientOnlyModuleUnavailable"
30
- ].join("\n");
31
- }
32
- function createClientOnlyStubPlugin() {
33
- return {
34
- name: "client-only-stub",
35
- setup(build) {
36
- build.onResolve({ filter: CLIENT_ONLY_SUFFIX_PATTERN }, (args) => {
37
- if (args.kind !== "dynamic-import") return null;
38
- if (!isClientOnlyModulePath(args.path)) return null;
39
- return { path: args.path, namespace: CLIENT_ONLY_STUB_NAMESPACE };
40
- });
41
- build.onLoad({ filter: /.*/, namespace: CLIENT_ONLY_STUB_NAMESPACE }, (args) => ({
42
- contents: renderClientOnlyModuleStub(args.path),
43
- loader: "js"
44
- }));
45
- }
46
- };
47
- }
48
- export {
49
- CLIENT_ONLY_STUB_NAMESPACE,
50
- createClientOnlyStubPlugin,
51
- encodeJsStringLiteral,
52
- isClientOnlyModulePath,
53
- renderClientOnlyModuleStub
54
- };
55
- //# sourceMappingURL=clientOnlyModules.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/bootstrap/clientOnlyModules.ts"],
4
- "sourcesContent": ["/**\n * Client-only modules (`*.client.tsx`) hold browser components and must never enter the\n * CLI bundle graph. App modules are bundled from source by `compileAndImport`, and esbuild\n * inlines dynamic imports into the single output file, hoisting the client file's static\n * imports to the top of the bundle. A wrapper such as `@open-mercato/ui/backend/charts`\n * would then execute on every CLI start and fail on bare Next.js specifiers that Node's\n * ESM resolver cannot resolve.\n *\n * Resolving these files to an inert stub keeps the owning `widget.ts` importable (the CLI\n * reads its metadata when seeding dashboards) while cutting the browser-only subgraph.\n *\n * Only `import()` expressions are stubbed. That is the documented way a server-side\n * `widget.ts` reaches its browser component (`lazyDashboardWidget(() => import('./widget.client'))`),\n * and its result is consumed as a namespace object, so a default-only stub is sufficient.\n * Static imports are left to the bundler: they may request named bindings the stub cannot\n * provide, which esbuild rejects with `No matching export`, failing the whole CLI bundle \u2014\n * the exact class of breakage this module exists to prevent. Restricting the rewrite to\n * dynamic imports also keeps server-side helpers that merely follow a `*.client.ts` naming\n * convention working untouched.\n */\n\nexport const CLIENT_ONLY_STUB_NAMESPACE = 'om-client-only-stub'\n\nconst LOCAL_IMPORT_PATTERN = /^(\\.{1,2}\\/|@\\/)/\nconst CLIENT_ONLY_SUFFIX_PATTERN = /(^|[\\\\/])[^\\\\/]+\\.client(\\.[cm]?[jt]sx?)?$/\n\n/**\n * Local (relative or `@/` aliased) imports of a `*.client` module. Bare package specifiers\n * are left alone because the bundler already marks them external, so they never get inlined.\n */\nexport function isClientOnlyModulePath(importPath: string): boolean {\n if (!LOCAL_IMPORT_PATTERN.test(importPath)) return false\n return CLIENT_ONLY_SUFFIX_PATTERN.test(importPath)\n}\n\n/**\n * Render a value as a JavaScript string literal safe to embed in generated source.\n * Everything outside printable ASCII is escaped to a `\\uXXXX` sequence, so no character\n * of the input can terminate the literal or be re-interpreted as code \u2014 `JSON.stringify`\n * alone is a JSON encoder, not a JavaScript-source escaper.\n */\nexport function encodeJsStringLiteral(value: string): string {\n let encoded = ''\n for (let index = 0; index < value.length; index += 1) {\n const char = value[index]\n if (char === '\"' || char === '\\\\') {\n encoded += `\\\\${char}`\n continue\n }\n const code = value.charCodeAt(index)\n if (code < 0x20 || code > 0x7e) {\n encoded += `\\\\u${code.toString(16).padStart(4, '0')}`\n continue\n }\n encoded += char\n }\n return `\"${encoded}\"`\n}\n\nexport function renderClientOnlyModuleStub(importPath: string): string {\n const message =\n `[internal] Client-only module ${importPath} is not available in the CLI runtime. ` +\n 'It is excluded from the CLI bundle because it renders browser components.'\n return [\n `function clientOnlyModuleUnavailable() { throw new Error(${encodeJsStringLiteral(message)}) }`,\n 'export default clientOnlyModuleUnavailable',\n ].join('\\n')\n}\n\nexport function createClientOnlyStubPlugin(): import('esbuild').Plugin {\n return {\n name: 'client-only-stub',\n setup(build) {\n build.onResolve({ filter: CLIENT_ONLY_SUFFIX_PATTERN }, (args) => {\n if (args.kind !== 'dynamic-import') return null\n if (!isClientOnlyModulePath(args.path)) return null\n return { path: args.path, namespace: CLIENT_ONLY_STUB_NAMESPACE }\n })\n build.onLoad({ filter: /.*/, namespace: CLIENT_ONLY_STUB_NAMESPACE }, (args) => ({\n contents: renderClientOnlyModuleStub(args.path),\n loader: 'js',\n }))\n },\n }\n}\n"],
5
- "mappings": "AAqBO,MAAM,6BAA6B;AAE1C,MAAM,uBAAuB;AAC7B,MAAM,6BAA6B;AAM5B,SAAS,uBAAuB,YAA6B;AAClE,MAAI,CAAC,qBAAqB,KAAK,UAAU,EAAG,QAAO;AACnD,SAAO,2BAA2B,KAAK,UAAU;AACnD;AAQO,SAAS,sBAAsB,OAAuB;AAC3D,MAAI,UAAU;AACd,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,GAAG;AACpD,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,SAAS,OAAO,SAAS,MAAM;AACjC,iBAAW,KAAK,IAAI;AACpB;AAAA,IACF;AACA,UAAM,OAAO,MAAM,WAAW,KAAK;AACnC,QAAI,OAAO,MAAQ,OAAO,KAAM;AAC9B,iBAAW,MAAM,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AACnD;AAAA,IACF;AACA,eAAW;AAAA,EACb;AACA,SAAO,IAAI,OAAO;AACpB;AAEO,SAAS,2BAA2B,YAA4B;AACrE,QAAM,UACJ,iCAAiC,UAAU;AAE7C,SAAO;AAAA,IACL,4DAA4D,sBAAsB,OAAO,CAAC;AAAA,IAC1F;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAEO,SAAS,6BAAuD;AACrE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,OAAO;AACX,YAAM,UAAU,EAAE,QAAQ,2BAA2B,GAAG,CAAC,SAAS;AAChE,YAAI,KAAK,SAAS,iBAAkB,QAAO;AAC3C,YAAI,CAAC,uBAAuB,KAAK,IAAI,EAAG,QAAO;AAC/C,eAAO,EAAE,MAAM,KAAK,MAAM,WAAW,2BAA2B;AAAA,MAClE,CAAC;AACD,YAAM,OAAO,EAAE,QAAQ,MAAM,WAAW,2BAA2B,GAAG,CAAC,UAAU;AAAA,QAC/E,UAAU,2BAA2B,KAAK,IAAI;AAAA,QAC9C,QAAQ;AAAA,MACV,EAAE;AAAA,IACJ;AAAA,EACF;AACF;",
6
- "names": []
7
- }
@@ -1,19 +0,0 @@
1
- import { parseBooleanWithDefault } from "../boolean.js";
2
- let alwaysConsistentFlag = null;
3
- function parseAlwaysConsistentEnv(raw) {
4
- return parseBooleanWithDefault(raw, false);
5
- }
6
- function isReadProjectionAlwaysConsistent() {
7
- if (alwaysConsistentFlag !== null) return alwaysConsistentFlag;
8
- alwaysConsistentFlag = parseAlwaysConsistentEnv(process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT);
9
- return alwaysConsistentFlag;
10
- }
11
- function __resetAlwaysConsistentCacheForTests() {
12
- alwaysConsistentFlag = null;
13
- }
14
- export {
15
- __resetAlwaysConsistentCacheForTests,
16
- isReadProjectionAlwaysConsistent,
17
- parseAlwaysConsistentEnv
18
- };
19
- //# sourceMappingURL=consistency.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/data/consistency.ts"],
4
- "sourcesContent": ["import { parseBooleanWithDefault } from '../boolean'\n\nlet alwaysConsistentFlag: boolean | null = null\n\nexport function parseAlwaysConsistentEnv(raw: string | undefined | null): boolean {\n return parseBooleanWithDefault(raw, false)\n}\n\nexport function isReadProjectionAlwaysConsistent(): boolean {\n if (alwaysConsistentFlag !== null) return alwaysConsistentFlag\n alwaysConsistentFlag = parseAlwaysConsistentEnv(process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT)\n return alwaysConsistentFlag\n}\n\nexport function __resetAlwaysConsistentCacheForTests(): void {\n alwaysConsistentFlag = null\n}\n"],
5
- "mappings": "AAAA,SAAS,+BAA+B;AAExC,IAAI,uBAAuC;AAEpC,SAAS,yBAAyB,KAAyC;AAChF,SAAO,wBAAwB,KAAK,KAAK;AAC3C;AAEO,SAAS,mCAA4C;AAC1D,MAAI,yBAAyB,KAAM,QAAO;AAC1C,yBAAuB,yBAAyB,QAAQ,IAAI,iCAAiC;AAC7F,SAAO;AACT;AAEO,SAAS,uCAA6C;AAC3D,yBAAuB;AACzB;",
6
- "names": []
7
- }
@@ -1,26 +0,0 @@
1
- function toMetadataList(value) {
2
- if (!value) return [];
3
- if (value instanceof Map) return Array.from(value.values());
4
- if (Array.isArray(value)) return value;
5
- if (typeof value === "object") return Object.values(value);
6
- return [];
7
- }
8
- function listEntityMetadataFromRegistry(registry) {
9
- const source = registry;
10
- if (!source) return [];
11
- if (typeof source.getAll === "function") {
12
- const entries = toMetadataList(source.getAll());
13
- if (entries.length) return entries;
14
- }
15
- return toMetadataList(source.metadata);
16
- }
17
- function listEntityMetadata(source) {
18
- const holder = source;
19
- if (!holder || typeof holder.getMetadata !== "function") return [];
20
- return listEntityMetadataFromRegistry(holder.getMetadata());
21
- }
22
- export {
23
- listEntityMetadata,
24
- listEntityMetadataFromRegistry
25
- };
26
- //# sourceMappingURL=entityMetadata.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/db/entityMetadata.ts"],
4
- "sourcesContent": ["import type { EntityMetadata } from '@mikro-orm/core'\n\ntype MetadataRegistryLike = {\n getAll?: () => unknown\n metadata?: unknown\n}\n\ntype MetadataHolder = {\n getMetadata?: () => unknown\n}\n\n/**\n * MikroORM v7 returns a `Map` from `MetadataStorage.getAll()`; v6 returned a plain\n * dictionary. `Object.values(someMap)` evaluates to `[]`, so a v6-era read of the\n * registry silently reports zero entities instead of failing loudly.\n */\nfunction toMetadataList(value: unknown): EntityMetadata[] {\n if (!value) return []\n if (value instanceof Map) return Array.from(value.values()) as EntityMetadata[]\n if (Array.isArray(value)) return value as EntityMetadata[]\n if (typeof value === 'object') return Object.values(value as Record<string, EntityMetadata>)\n return []\n}\n\nexport function listEntityMetadataFromRegistry(registry: unknown): EntityMetadata[] {\n const source = registry as MetadataRegistryLike | null | undefined\n if (!source) return []\n if (typeof source.getAll === 'function') {\n const entries = toMetadataList(source.getAll())\n if (entries.length) return entries\n }\n return toMetadataList(source.metadata)\n}\n\n/**\n * List every discovered entity metadata from an `EntityManager` or `MikroORM` instance.\n * Returns an empty array when the source exposes no metadata registry.\n */\nexport function listEntityMetadata(source: unknown): EntityMetadata[] {\n const holder = source as MetadataHolder | null | undefined\n if (!holder || typeof holder.getMetadata !== 'function') return []\n return listEntityMetadataFromRegistry(holder.getMetadata())\n}\n"],
5
- "mappings": "AAgBA,SAAS,eAAe,OAAkC;AACxD,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,MAAI,iBAAiB,IAAK,QAAO,MAAM,KAAK,MAAM,OAAO,CAAC;AAC1D,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,OAAO,KAAuC;AAC3F,SAAO,CAAC;AACV;AAEO,SAAS,+BAA+B,UAAqC;AAClF,QAAM,SAAS;AACf,MAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,MAAI,OAAO,OAAO,WAAW,YAAY;AACvC,UAAM,UAAU,eAAe,OAAO,OAAO,CAAC;AAC9C,QAAI,QAAQ,OAAQ,QAAO;AAAA,EAC7B;AACA,SAAO,eAAe,OAAO,QAAQ;AACvC;AAMO,SAAS,mBAAmB,QAAmC;AACpE,QAAM,SAAS;AACf,MAAI,CAAC,UAAU,OAAO,OAAO,gBAAgB,WAAY,QAAO,CAAC;AACjE,SAAO,+BAA+B,OAAO,YAAY,CAAC;AAC5D;",
6
- "names": []
7
- }
@@ -1,29 +0,0 @@
1
- const GLOBAL_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/shared.loggerExtension");
2
- function store() {
3
- const globalStore = globalThis;
4
- let current = globalStore[GLOBAL_KEY];
5
- if (!current) {
6
- current = {};
7
- globalStore[GLOBAL_KEY] = current;
8
- }
9
- return current;
10
- }
11
- function registerLoggerExtension(extension) {
12
- store().active = extension;
13
- return () => {
14
- const current = store();
15
- if (current.active === extension) current.active = void 0;
16
- };
17
- }
18
- function getLoggerExtension() {
19
- return store().active;
20
- }
21
- function resetLoggerExtension() {
22
- store().active = void 0;
23
- }
24
- export {
25
- getLoggerExtension,
26
- registerLoggerExtension,
27
- resetLoggerExtension
28
- };
29
- //# sourceMappingURL=extension.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/logger/extension.ts"],
4
- "sourcesContent": ["import type { LogLevel } from './level'\n\nexport type LoggerExtensionRecord = {\n level: LogLevel\n namespace: string\n message: string\n fields: Record<string, unknown>\n time: number\n}\n\nexport type LoggerExtension = {\n /**\n * Add process-local context (for example trace/span ids) to both the local\n * line and the optional remote sink.\n */\n enrich?(): Record<string, unknown> | undefined\n /**\n * Observe a record after the local transport writes it. Implementations must\n * never throw into application code.\n */\n emit?(record: LoggerExtensionRecord): void\n}\n\nconst GLOBAL_KEY = Symbol.for('@open-mercato/shared.loggerExtension')\n\ntype LoggerExtensionStore = {\n active?: LoggerExtension\n}\n\nfunction store(): LoggerExtensionStore {\n const globalStore = globalThis as unknown as Record<symbol, LoggerExtensionStore | undefined>\n let current = globalStore[GLOBAL_KEY]\n if (!current) {\n current = {}\n globalStore[GLOBAL_KEY] = current\n }\n return current\n}\n\n/**\n * Register the single process-wide logger extension. The returned disposer only\n * clears the extension when it is still the active registration.\n */\nexport function registerLoggerExtension(extension: LoggerExtension): () => void {\n store().active = extension\n return () => {\n const current = store()\n if (current.active === extension) current.active = undefined\n }\n}\n\nexport function getLoggerExtension(): LoggerExtension | undefined {\n return store().active\n}\n\n/** Test-only: clear the process-wide extension. */\nexport function resetLoggerExtension(): void {\n store().active = undefined\n}\n"],
5
- "mappings": "AAuBA,MAAM,aAAa,uBAAO,IAAI,sCAAsC;AAMpE,SAAS,QAA8B;AACrC,QAAM,cAAc;AACpB,MAAI,UAAU,YAAY,UAAU;AACpC,MAAI,CAAC,SAAS;AACZ,cAAU,CAAC;AACX,gBAAY,UAAU,IAAI;AAAA,EAC5B;AACA,SAAO;AACT;AAMO,SAAS,wBAAwB,WAAwC;AAC9E,QAAM,EAAE,SAAS;AACjB,SAAO,MAAM;AACX,UAAM,UAAU,MAAM;AACtB,QAAI,QAAQ,WAAW,UAAW,SAAQ,SAAS;AAAA,EACrD;AACF;AAEO,SAAS,qBAAkD;AAChE,SAAO,MAAM,EAAE;AACjB;AAGO,SAAS,uBAA6B;AAC3C,QAAM,EAAE,SAAS;AACnB;",
6
- "names": []
7
- }
@@ -1,47 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { getBackendRouteManifests } from "../../modules/registry.js";
3
- import { getModules } from "./registry.js";
4
- const UNKNOWN_FINGERPRINT = "unknown";
5
- let cachedFingerprint = null;
6
- let cachedManifestsRef = null;
7
- let cachedModulesRef = null;
8
- function readModulesRef() {
9
- try {
10
- return getModules();
11
- } catch {
12
- return null;
13
- }
14
- }
15
- function byCodeUnit(a, b) {
16
- return a < b ? -1 : a > b ? 1 : 0;
17
- }
18
- function describeModules(modules) {
19
- if (!modules) return [];
20
- return modules.map((mod) => {
21
- const features = (Array.isArray(mod.features) ? mod.features : []).filter((feature) => feature && typeof feature.id === "string" && feature.id.length > 0).map((feature) => `${feature.id}:${feature.module || mod.id}`).sort(byCodeUnit);
22
- return JSON.stringify({ id: mod.id, features });
23
- }).sort(byCodeUnit);
24
- }
25
- function getModuleSurfaceFingerprint() {
26
- const manifests = getBackendRouteManifests();
27
- const modulesRef = readModulesRef();
28
- if (cachedFingerprint !== null && cachedManifestsRef === manifests && cachedModulesRef === modulesRef) {
29
- return cachedFingerprint;
30
- }
31
- let fingerprint;
32
- try {
33
- const modules = describeModules(modulesRef);
34
- const routes = manifests.map((route) => JSON.stringify(route)).sort(byCodeUnit);
35
- fingerprint = createHash("sha1").update(JSON.stringify({ modules, routes })).digest("hex").slice(0, 12);
36
- } catch {
37
- fingerprint = UNKNOWN_FINGERPRINT;
38
- }
39
- cachedManifestsRef = manifests;
40
- cachedModulesRef = modulesRef;
41
- cachedFingerprint = fingerprint;
42
- return fingerprint;
43
- }
44
- export {
45
- getModuleSurfaceFingerprint
46
- };
47
- //# sourceMappingURL=surfaceFingerprint.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/modules/surfaceFingerprint.ts"],
4
- "sourcesContent": ["/**\n * Deploy-time fingerprint of the registered module surface.\n *\n * Some cached payloads are derived from state that only changes when the app\n * is redeployed \u2014 the enabled module set and the backend route manifest. That\n * state has no database write to hang a tag invalidation off, so a cache key\n * that omits it keeps serving the pre-deploy payload to anyone with a warm\n * entry (see `/api/auth/admin/nav`, whose payload embeds\n * `filterGrantsByEnabledModules(...)` computed at write time).\n *\n * Mixing this fingerprint into such keys makes new processes write to new\n * keys, so old and new pods coexist during a rolling deploy without a purge\n * step and without any ordering constraint between purge and rollout.\n *\n * Covered inputs: the enabled module ids, each module's declared `features`\n * (which drive `filterGrantsByEnabledModules`, including the `*` superadmin\n * expansion and off-convention prefixes), and every JSON-serializable field\n * of the backend route manifest.\n *\n * NOT covered: a route's `icon`. It is a React element whose `type` is a\n * function, which `JSON.stringify` drops \u2014 two different icon components\n * serialize identically. Swapping an icon heals on the caller's TTL, not on\n * the fingerprint. Callers MUST therefore still pass a `ttl`.\n */\nimport { createHash } from 'node:crypto'\nimport type { Module } from '../../modules/registry'\nimport { getBackendRouteManifests } from '../../modules/registry'\nimport { getModules } from './registry'\n\nconst UNKNOWN_FINGERPRINT = 'unknown'\n\nlet cachedFingerprint: string | null = null\nlet cachedManifestsRef: unknown = null\nlet cachedModulesRef: unknown = null\n\nfunction readModulesRef(): readonly Module[] | null {\n try {\n return getModules()\n } catch {\n return null\n }\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0\n}\n\nfunction describeModules(modules: readonly Module[] | null): string[] {\n if (!modules) return []\n return modules\n .map((mod) => {\n const features = (Array.isArray(mod.features) ? mod.features : [])\n .filter((feature) => feature && typeof feature.id === 'string' && feature.id.length > 0)\n .map((feature) => `${feature.id}:${feature.module || mod.id}`)\n .sort(byCodeUnit)\n return JSON.stringify({ id: mod.id, features })\n })\n .sort(byCodeUnit)\n}\n\nexport function getModuleSurfaceFingerprint(): string {\n const manifests = getBackendRouteManifests()\n const modulesRef = readModulesRef()\n if (cachedFingerprint !== null && cachedManifestsRef === manifests && cachedModulesRef === modulesRef) {\n return cachedFingerprint\n }\n // The manifest carries third-party route metadata and module-scope React\n // elements, so serialization is not fully under this repo's control. Every\n // other fallible step in the nav handler degrades rather than throws, and a\n // throw here would blank the entire admin chrome \u2014 fall back to a constant,\n // which is exactly the pre-fingerprint behavior.\n let fingerprint: string\n try {\n const modules = describeModules(modulesRef)\n const routes = manifests.map((route) => JSON.stringify(route)).sort(byCodeUnit)\n fingerprint = createHash('sha1')\n .update(JSON.stringify({ modules, routes }))\n .digest('hex')\n .slice(0, 12)\n } catch {\n fingerprint = UNKNOWN_FINGERPRINT\n }\n cachedManifestsRef = manifests\n cachedModulesRef = modulesRef\n cachedFingerprint = fingerprint\n return fingerprint\n}\n"],
5
- "mappings": "AAwBA,SAAS,kBAAkB;AAE3B,SAAS,gCAAgC;AACzC,SAAS,kBAAkB;AAE3B,MAAM,sBAAsB;AAE5B,IAAI,oBAAmC;AACvC,IAAI,qBAA8B;AAClC,IAAI,mBAA4B;AAEhC,SAAS,iBAA2C;AAClD,MAAI;AACF,WAAO,WAAW;AAAA,EACpB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,WAAW,GAAW,GAAmB;AAChD,SAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI;AAClC;AAEA,SAAS,gBAAgB,SAA6C;AACpE,MAAI,CAAC,QAAS,QAAO,CAAC;AACtB,SAAO,QACJ,IAAI,CAAC,QAAQ;AACZ,UAAM,YAAY,MAAM,QAAQ,IAAI,QAAQ,IAAI,IAAI,WAAW,CAAC,GAC7D,OAAO,CAAC,YAAY,WAAW,OAAO,QAAQ,OAAO,YAAY,QAAQ,GAAG,SAAS,CAAC,EACtF,IAAI,CAAC,YAAY,GAAG,QAAQ,EAAE,IAAI,QAAQ,UAAU,IAAI,EAAE,EAAE,EAC5D,KAAK,UAAU;AAClB,WAAO,KAAK,UAAU,EAAE,IAAI,IAAI,IAAI,SAAS,CAAC;AAAA,EAChD,CAAC,EACA,KAAK,UAAU;AACpB;AAEO,SAAS,8BAAsC;AACpD,QAAM,YAAY,yBAAyB;AAC3C,QAAM,aAAa,eAAe;AAClC,MAAI,sBAAsB,QAAQ,uBAAuB,aAAa,qBAAqB,YAAY;AACrG,WAAO;AAAA,EACT;AAMA,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,gBAAgB,UAAU;AAC1C,UAAM,SAAS,UAAU,IAAI,CAAC,UAAU,KAAK,UAAU,KAAK,CAAC,EAAE,KAAK,UAAU;AAC9E,kBAAc,WAAW,MAAM,EAC5B,OAAO,KAAK,UAAU,EAAE,SAAS,OAAO,CAAC,CAAC,EAC1C,OAAO,KAAK,EACZ,MAAM,GAAG,EAAE;AAAA,EAChB,QAAQ;AACN,kBAAc;AAAA,EAChB;AACA,uBAAqB;AACrB,qBAAmB;AACnB,sBAAoB;AACpB,SAAO;AACT;",
6
- "names": []
7
- }
@@ -1,45 +0,0 @@
1
- import { createLogger } from "../logger/index.js";
2
- const logger = createLogger("shared").child({ component: "query" });
3
- const WARN_CACHE_CAP = 5e3;
4
- const warned = /* @__PURE__ */ new Set();
5
- const warnKey = (entity, tenantId, field) => `${entity}|${tenantId ?? "null"}|${field}`;
6
- const normalizeColumnName = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
7
- function resetCiphertextLikeWarnCache() {
8
- warned.clear();
9
- }
10
- async function warnOnCiphertextLikeFallback(params) {
11
- const { entity, tenantId, reason, service } = params;
12
- if (!service || service.isEnabled?.() === false) return;
13
- if (typeof service.getEncryptedFieldNames !== "function") return;
14
- const candidates = params.fields.filter((field) => typeof field === "string" && !field.startsWith("cf:")).map((field) => normalizeColumnName(field)).filter((field, index, all) => field.length > 0 && all.indexOf(field) === index).filter((field) => !warned.has(warnKey(entity, tenantId, field)));
15
- if (!candidates.length) return;
16
- try {
17
- const encrypted = new Set(
18
- (await service.getEncryptedFieldNames(entity, tenantId, null)).map(
19
- (field) => normalizeColumnName(String(field))
20
- )
21
- );
22
- for (const field of candidates) {
23
- if (!encrypted.has(field)) continue;
24
- if (warned.size >= WARN_CACHE_CAP) warned.clear();
25
- warned.add(warnKey(entity, tenantId, field));
26
- logger.warn("Text search filter cannot match an encrypted column", {
27
- entity,
28
- field,
29
- reason,
30
- hint: reason === "search-disabled" ? "OM_SEARCH_ENABLED is off, so the filter runs as ILIKE against ciphertext and matches nothing." : reason === "no-indexable-tokens" ? "The filter value produced no search tokens, so it runs as ILIKE against ciphertext and matches nothing. Use at least OM_SEARCH_MIN_LEN indexable characters, or filter on the deterministic hash column." : "No search_tokens exist for this entity and scope, so the filter runs as ILIKE against ciphertext and matches nothing. Index the entity through query_index and reindex, or filter on the deterministic hash column."
31
- });
32
- }
33
- } catch (err) {
34
- logger.debug("Ciphertext search warning check failed", {
35
- entity,
36
- err: err instanceof Error ? err.message : String(err)
37
- });
38
- }
39
- }
40
- export {
41
- normalizeColumnName,
42
- resetCiphertextLikeWarnCache,
43
- warnOnCiphertextLikeFallback
44
- };
45
- //# sourceMappingURL=ciphertext-search-warning.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/query/ciphertext-search-warning.ts"],
4
- "sourcesContent": ["import { createLogger } from '../logger'\nimport type { EntityId } from '../../modules/entities'\n\nconst logger = createLogger('shared').child({ component: 'query' })\n\n/**\n * Minimal slice of `TenantDataEncryptionService` needed to decide whether a\n * text filter targets an encrypted column.\n */\nexport type CiphertextWarningEncryptionService = {\n getEncryptedFieldNames?: (\n entityId: EntityId,\n tenantId?: string | null,\n organizationId?: string | null,\n ) => Promise<readonly string[]>\n isEnabled?: () => boolean\n} | null | undefined\n\nexport type CiphertextLikeFallbackReason = 'no-indexable-tokens' | 'no-search-tokens' | 'search-disabled'\n\n// One warning per entity/tenant/field per process \u2014 the fallback repeats on\n// every request, and an operator only needs to learn about it once.\nconst WARN_CACHE_CAP = 5000\nconst warned = new Set<string>()\n\nconst warnKey = (entity: string, tenantId: string | null, field: string): string =>\n `${entity}|${tenantId ?? 'null'}|${field}`\n\nexport const normalizeColumnName = (value: string): string =>\n value.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()\n\n/** Test seam: the warn-once cache is process-global by design. */\nexport function resetCiphertextLikeWarnCache(): void {\n warned.clear()\n}\n\n/**\n * Warn when a `like`/`ilike` filter is about to run against a column that an\n * encryption map covers while the token index is unavailable.\n *\n * Without this the query degrades silently: the predicate compares a plaintext\n * pattern against ciphertext, matches nothing, and the endpoint returns an\n * empty page that is indistinguishable from a genuine no-result. Issue #2990.\n *\n * Never throws and never blocks the query \u2014 a failure here is logged at debug\n * level and the caller proceeds unchanged.\n */\nexport async function warnOnCiphertextLikeFallback(params: {\n entity: string\n fields: readonly string[]\n tenantId: string | null\n reason: CiphertextLikeFallbackReason\n service: CiphertextWarningEncryptionService\n}): Promise<void> {\n const { entity, tenantId, reason, service } = params\n if (!service || service.isEnabled?.() === false) return\n if (typeof service.getEncryptedFieldNames !== 'function') return\n\n const candidates = params.fields\n .filter((field) => typeof field === 'string' && !field.startsWith('cf:'))\n .map((field) => normalizeColumnName(field))\n .filter((field, index, all) => field.length > 0 && all.indexOf(field) === index)\n .filter((field) => !warned.has(warnKey(entity, tenantId, field)))\n if (!candidates.length) return\n\n try {\n // `organizationId: null` resolves the global map plus every per-organization\n // override, so a field encrypted for any organization is reported.\n const encrypted = new Set(\n (await service.getEncryptedFieldNames(entity, tenantId, null)).map((field) =>\n normalizeColumnName(String(field)),\n ),\n )\n for (const field of candidates) {\n if (!encrypted.has(field)) continue\n if (warned.size >= WARN_CACHE_CAP) warned.clear()\n warned.add(warnKey(entity, tenantId, field))\n logger.warn('Text search filter cannot match an encrypted column', {\n entity,\n field,\n reason,\n hint: reason === 'search-disabled'\n ? 'OM_SEARCH_ENABLED is off, so the filter runs as ILIKE against ciphertext and matches nothing.'\n : reason === 'no-indexable-tokens'\n ? 'The filter value produced no search tokens, so it runs as ILIKE against ciphertext and matches nothing. Use at least OM_SEARCH_MIN_LEN indexable characters, or filter on the deterministic hash column.'\n : 'No search_tokens exist for this entity and scope, so the filter runs as ILIKE against ciphertext and matches nothing. Index the entity through query_index and reindex, or filter on the deterministic hash column.',\n })\n }\n } catch (err) {\n logger.debug('Ciphertext search warning check failed', {\n entity,\n err: err instanceof Error ? err.message : String(err),\n })\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAG7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAmBlE,MAAM,iBAAiB;AACvB,MAAM,SAAS,oBAAI,IAAY;AAE/B,MAAM,UAAU,CAAC,QAAgB,UAAyB,UACxD,GAAG,MAAM,IAAI,YAAY,MAAM,IAAI,KAAK;AAEnC,MAAM,sBAAsB,CAAC,UAClC,MAAM,QAAQ,sBAAsB,OAAO,EAAE,YAAY;AAGpD,SAAS,+BAAqC;AACnD,SAAO,MAAM;AACf;AAaA,eAAsB,6BAA6B,QAMjC;AAChB,QAAM,EAAE,QAAQ,UAAU,QAAQ,QAAQ,IAAI;AAC9C,MAAI,CAAC,WAAW,QAAQ,YAAY,MAAM,MAAO;AACjD,MAAI,OAAO,QAAQ,2BAA2B,WAAY;AAE1D,QAAM,aAAa,OAAO,OACvB,OAAO,CAAC,UAAU,OAAO,UAAU,YAAY,CAAC,MAAM,WAAW,KAAK,CAAC,EACvE,IAAI,CAAC,UAAU,oBAAoB,KAAK,CAAC,EACzC,OAAO,CAAC,OAAO,OAAO,QAAQ,MAAM,SAAS,KAAK,IAAI,QAAQ,KAAK,MAAM,KAAK,EAC9E,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,QAAQ,QAAQ,UAAU,KAAK,CAAC,CAAC;AAClE,MAAI,CAAC,WAAW,OAAQ;AAExB,MAAI;AAGF,UAAM,YAAY,IAAI;AAAA,OACnB,MAAM,QAAQ,uBAAuB,QAAQ,UAAU,IAAI,GAAG;AAAA,QAAI,CAAC,UAClE,oBAAoB,OAAO,KAAK,CAAC;AAAA,MACnC;AAAA,IACF;AACA,eAAW,SAAS,YAAY;AAC9B,UAAI,CAAC,UAAU,IAAI,KAAK,EAAG;AAC3B,UAAI,OAAO,QAAQ,eAAgB,QAAO,MAAM;AAChD,aAAO,IAAI,QAAQ,QAAQ,UAAU,KAAK,CAAC;AAC3C,aAAO,KAAK,uDAAuD;AAAA,QACjE;AAAA,QACA;AAAA,QACA;AAAA,QACA,MAAM,WAAW,oBACb,kGACA,WAAW,wBACT,6MACA;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO,MAAM,0CAA0C;AAAA,MACrD;AAAA,MACA,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACtD,CAAC;AAAA,EACH;AACF;",
6
- "names": []
7
- }
@@ -1,14 +0,0 @@
1
- import { parseBooleanToken } from "../boolean.js";
2
- const SEARCH_AUTO_INDEX_CONFIG_MODULE = "vector";
3
- const SEARCH_AUTO_INDEX_CONFIG_KEY = "auto_index_enabled";
4
- function envDisablesAutoIndexing() {
5
- const raw = process.env.OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING ?? process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING;
6
- if (!raw) return false;
7
- return parseBooleanToken(raw) === true;
8
- }
9
- export {
10
- SEARCH_AUTO_INDEX_CONFIG_KEY,
11
- SEARCH_AUTO_INDEX_CONFIG_MODULE,
12
- envDisablesAutoIndexing
13
- };
14
- //# sourceMappingURL=auto-indexing.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/search/auto-indexing.ts"],
4
- "sourcesContent": ["import { parseBooleanToken } from '../boolean'\n\n/**\n * Module id backing the tenant-scoped vector auto-indexing switch. Kept as `vector`\n * (rather than `search`) for backwards compatibility with rows written before the\n * search module absorbed the vector settings.\n */\nexport const SEARCH_AUTO_INDEX_CONFIG_MODULE = 'vector'\nexport const SEARCH_AUTO_INDEX_CONFIG_KEY = 'auto_index_enabled'\n\n/**\n * Instance-wide kill switch for vector auto-indexing. Lives in `@open-mercato/shared`\n * so consumers outside `@open-mercato/search` (for example the query index status page)\n * can report vector coverage honestly without duplicating the env-var contract.\n */\nexport function envDisablesAutoIndexing(): boolean {\n const raw =\n process.env.OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING ??\n process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING\n if (!raw) return false\n return parseBooleanToken(raw) === true\n}\n"],
5
- "mappings": "AAAA,SAAS,yBAAyB;AAO3B,MAAM,kCAAkC;AACxC,MAAM,+BAA+B;AAOrC,SAAS,0BAAmC;AACjD,QAAM,MACJ,QAAQ,IAAI,yCACZ,QAAQ,IAAI;AACd,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,kBAAkB,GAAG,MAAM;AACpC;",
6
- "names": []
7
- }
@@ -1,107 +0,0 @@
1
- import { sql } from "kysely";
2
- import { parseNumberWithDefault } from "@open-mercato/shared/lib/number";
3
- function isSearchFilterOp(op) {
4
- return op === "like" || op === "ilike";
5
- }
6
- function hasSearchFilter(filters) {
7
- return filters.some((filter) => isSearchFilterOp(filter.op));
8
- }
9
- function orgScopeKey(scope) {
10
- if (!scope) return "none";
11
- return `${scope.includeNull ? "1" : "0"}:${[...scope.ids].sort((left, right) => left.localeCompare(right)).join(",")}`;
12
- }
13
- const PRESENCE_CACHE_DEFAULT_TTL_MS = 3e4;
14
- const PRESENCE_CACHE_MAX_ENTRIES = 1e4;
15
- const presenceCache = /* @__PURE__ */ new Map();
16
- function resolvePresenceCacheTtlMs() {
17
- return parseNumberWithDefault(process.env.OM_SEARCH_TOKEN_PRESENCE_CACHE_MS, PRESENCE_CACHE_DEFAULT_TTL_MS, { integer: true, min: 0 });
18
- }
19
- function storePresence(key, value, ttlMs) {
20
- if (presenceCache.size >= PRESENCE_CACHE_MAX_ENTRIES) {
21
- const now = Date.now();
22
- for (const [entryKey, entry] of presenceCache) {
23
- if (entry.expiresAt <= now) presenceCache.delete(entryKey);
24
- }
25
- if (presenceCache.size >= PRESENCE_CACHE_MAX_ENTRIES) presenceCache.clear();
26
- }
27
- presenceCache.set(key, { value, expiresAt: Date.now() + ttlMs });
28
- }
29
- function clearSearchTokenPresenceCache() {
30
- presenceCache.clear();
31
- }
32
- function createSearchTokenAvailability(deps) {
33
- const tablePresence = /* @__PURE__ */ new Map();
34
- const tokenPresence = /* @__PURE__ */ new Map();
35
- const tableExists = (table) => {
36
- const cached = tablePresence.get(table);
37
- if (cached) return cached;
38
- const probe = (async () => {
39
- const row = await deps.getDb().selectFrom("information_schema.tables").select(sql`1`.as("one")).where("table_name", "=", table).limit(1).executeTakeFirst();
40
- return !!row;
41
- })();
42
- tablePresence.set(table, probe);
43
- probe.catch(() => tablePresence.delete(table));
44
- return probe;
45
- };
46
- const probeTokens = async (entity, tenantId, orgScope) => {
47
- let query = deps.getDb().selectFrom("search_tokens").select(sql`1`.as("one")).where("entity_type", "=", entity);
48
- query = tenantId == null ? query.where("tenant_id", "is", null) : query.where("tenant_id", "=", tenantId);
49
- if (orgScope) {
50
- query = deps.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
51
- }
52
- const row = await query.limit(1).executeTakeFirst();
53
- return !!row;
54
- };
55
- const hasTokens = (entity, tenantId, orgScope) => {
56
- const key = `${entity}|${tenantId ?? "__null__"}|${orgScopeKey(orgScope)}`;
57
- const ttlMs = resolvePresenceCacheTtlMs();
58
- if (ttlMs > 0) {
59
- const entry = presenceCache.get(key);
60
- if (entry && entry.expiresAt > Date.now()) return Promise.resolve(entry.value);
61
- }
62
- const cached = tokenPresence.get(key);
63
- if (cached) return cached;
64
- const probe = (async () => {
65
- try {
66
- const value = await probeTokens(entity, tenantId, orgScope);
67
- if (ttlMs > 0) storePresence(key, value, ttlMs);
68
- return value;
69
- } catch (err) {
70
- deps.logDebug("search:has-tokens-error", {
71
- entity,
72
- tenantId,
73
- organizationScope: orgScope,
74
- error: err instanceof Error ? err.message : String(err)
75
- });
76
- return false;
77
- }
78
- })();
79
- tokenPresence.set(key, probe);
80
- return probe;
81
- };
82
- return {
83
- staticEnabled: async () => deps.getConfig().enabled && await tableExists("search_tokens"),
84
- hasTokens,
85
- anySourceHasTokens: async (sources, tenantId, orgScope) => {
86
- for (const source of sources) {
87
- const ok = await hasTokens(source.entity, tenantId, orgScope);
88
- deps.logDebug("search:source-has-tokens", {
89
- entity: source.entity,
90
- recordIdColumn: source.recordIdColumn,
91
- tenantId,
92
- organizationScope: orgScope,
93
- hasTokens: ok
94
- });
95
- if (ok) return true;
96
- }
97
- return false;
98
- }
99
- };
100
- }
101
- export {
102
- clearSearchTokenPresenceCache,
103
- createSearchTokenAvailability,
104
- hasSearchFilter,
105
- isSearchFilterOp
106
- };
107
- //# sourceMappingURL=availability.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/search/availability.ts"],
4
- "sourcesContent": ["import { sql } from 'kysely'\nimport { parseNumberWithDefault } from '@open-mercato/shared/lib/number'\n\nexport type OrganizationScope = { ids: string[]; includeNull: boolean }\n\nexport type SearchTokenSourceRef = { entity: string; recordIdColumn?: string }\n\ntype ProbeExpression = object | string | readonly string[] | null\n\nexport type SearchTokenProbeQueryBuilder = {\n select: (selection: ProbeExpression) => SearchTokenProbeQueryBuilder\n where: (column: ProbeExpression, operator?: string, value?: ProbeExpression) => SearchTokenProbeQueryBuilder\n limit: (count: number) => SearchTokenProbeQueryBuilder\n executeTakeFirst: () => Promise<object | undefined>\n}\n\nexport type SearchTokenProbeDb = { selectFrom: (table: string) => SearchTokenProbeQueryBuilder }\n\nexport type SearchTokenAvailabilityDebugPayload = {\n entity: string\n tenantId: string | null\n organizationScope?: OrganizationScope | null\n recordIdColumn?: string\n hasTokens?: boolean\n error?: string\n}\n\nexport type SearchTokenAvailabilityDeps = {\n getDb: () => SearchTokenProbeDb\n getConfig: () => { enabled: boolean }\n applyOrganizationScope: (\n query: SearchTokenProbeQueryBuilder,\n column: string,\n scope: OrganizationScope,\n ) => SearchTokenProbeQueryBuilder\n logDebug: (event: string, payload: SearchTokenAvailabilityDebugPayload) => void\n}\n\nexport type SearchTokenAvailability = {\n /**\n * The cheap, statically-known half of the decision: search is configured on\n * AND the `search_tokens` table exists. Safe to resolve eagerly (consumers\n * like custom-field source attachment need it before any filter is\n * inspected); the table probe is memoized per instance.\n */\n staticEnabled: () => Promise<boolean>\n /**\n * The expensive half: does `search_tokens` hold any row for this\n * (entity, tenant, organization scope)? Historically the `LIMIT 1` probe\n * could degenerate into a seq scan on a large table (#4723), so callers MUST\n * only ask when the query actually carries a like/ilike filter \u2014 use\n * `hasSearchFilter` for the gate. Three layers keep it cheap for the end\n * user: index-usable predicates (no `IS NOT DISTINCT FROM`) served by the\n * dedicated `search_tokens_presence_idx (entity_type, tenant_id,\n * organization_id)` prefix \u2014 which makes the MISS as cheap as the hit \u2014 a\n * per-request instance memo, and a process-level TTL cache\n * (`OM_SEARCH_TOKEN_PRESENCE_CACHE_MS`, default 30s) that amortizes the\n * probe across requests. Probe errors log `search:has-tokens-error`,\n * resolve to `false`, and are never TTL-cached.\n */\n hasTokens: (entity: string, tenantId: string | null, orgScope?: OrganizationScope | null) => Promise<boolean>\n /** First-hit sweep over token sources; logs `search:source-has-tokens` per probed source. */\n anySourceHasTokens: (\n sources: SearchTokenSourceRef[],\n tenantId: string | null,\n orgScope?: OrganizationScope | null,\n ) => Promise<boolean>\n}\n\nexport function isSearchFilterOp(op: string | null | undefined): boolean {\n return op === 'like' || op === 'ilike'\n}\n\n/**\n * The single definition of \"this query actually searches\". Every consumer of\n * the token-availability answer sits behind a like/ilike guard, so when this\n * returns `false` the `hasTokens` probe's answer would never be read \u2014 gate\n * the probe on it.\n */\nexport function hasSearchFilter(filters: ReadonlyArray<{ op?: string | null }>): boolean {\n return filters.some((filter) => isSearchFilterOp(filter.op))\n}\n\nfunction orgScopeKey(scope: OrganizationScope | null | undefined): string {\n if (!scope) return 'none'\n return `${scope.includeNull ? '1' : '0'}:${[...scope.ids].sort((left, right) => left.localeCompare(right)).join(',')}`\n}\n\nconst PRESENCE_CACHE_DEFAULT_TTL_MS = 30_000\nconst PRESENCE_CACHE_MAX_ENTRIES = 10_000\n\n// Process-level TTL cache for the token-presence answer. Module-level (process-global) on\n// purpose: `createRequestContainer` builds fresh engines \u2014 and with them fresh resolver\n// instances \u2014 per request, so an instance-scoped memo alone re-pays the probe on every\n// request. On a large `search_tokens` one probe can be pathologically expensive (#4723),\n// so the answer is amortized across requests here and only re-checked once per TTL.\n// Staleness contract: a stale `false` keeps like/ilike on the plain-column fallback for up\n// to the TTL after an entity's first tokens are written; a stale `true` routes search\n// through an emptied token set for up to the TTL after a purge. Both converge within the\n// TTL; set OM_SEARCH_TOKEN_PRESENCE_CACHE_MS=0 to disable and probe per request again.\nconst presenceCache = new Map<string, { value: boolean; expiresAt: number }>()\n\nfunction resolvePresenceCacheTtlMs(): number {\n return parseNumberWithDefault(process.env.OM_SEARCH_TOKEN_PRESENCE_CACHE_MS, PRESENCE_CACHE_DEFAULT_TTL_MS, { integer: true, min: 0 })\n}\n\nfunction storePresence(key: string, value: boolean, ttlMs: number): void {\n if (presenceCache.size >= PRESENCE_CACHE_MAX_ENTRIES) {\n const now = Date.now()\n for (const [entryKey, entry] of presenceCache) {\n if (entry.expiresAt <= now) presenceCache.delete(entryKey)\n }\n if (presenceCache.size >= PRESENCE_CACHE_MAX_ENTRIES) presenceCache.clear()\n }\n presenceCache.set(key, { value, expiresAt: Date.now() + ttlMs })\n}\n\nexport function clearSearchTokenPresenceCache(): void {\n presenceCache.clear()\n}\n\n/**\n * One place that answers \"is token search usable here?\" for both query\n * engines, instead of each hand-assembling config + table-existence +\n * token-presence probes with private duplicate helpers and ad-hoc memo maps.\n *\n * Memoization is per instance; engines are constructed per request\n * (`createRequestContainer`), so entries never outlive a request \u2014 the same\n * staleness contract the engines' previous per-query join maps had. A\n * rejected table probe is evicted so the next call retries instead of\n * observing a poisoned cache entry.\n */\nexport function createSearchTokenAvailability(deps: SearchTokenAvailabilityDeps): SearchTokenAvailability {\n const tablePresence = new Map<string, Promise<boolean>>()\n const tokenPresence = new Map<string, Promise<boolean>>()\n\n const tableExists = (table: string): Promise<boolean> => {\n const cached = tablePresence.get(table)\n if (cached) return cached\n const probe = (async () => {\n const row = await deps.getDb()\n .selectFrom('information_schema.tables')\n .select(sql<number>`1`.as('one'))\n .where('table_name', '=', table)\n .limit(1)\n .executeTakeFirst()\n return !!row\n })()\n tablePresence.set(table, probe)\n probe.catch(() => tablePresence.delete(table))\n return probe\n }\n\n const probeTokens = async (\n entity: string,\n tenantId: string | null,\n orgScope?: OrganizationScope | null,\n ): Promise<boolean> => {\n let query = deps.getDb()\n .selectFrom('search_tokens')\n .select(sql<number>`1`.as('one'))\n .where('entity_type', '=', entity)\n // Deliberately `= / IS NULL` instead of `IS NOT DISTINCT FROM` (identical semantics\n // for a string|null tenant): the latter cannot serve as an index condition, which is\n // part of why the planner degraded this probe to a seq scan on large tables (#4723).\n // With plain predicates the probe is a pure prefix seek on\n // `search_tokens_presence_idx (entity_type, tenant_id, organization_id)`, making the\n // miss as cheap as the hit.\n query = tenantId == null\n ? query.where('tenant_id', 'is', null)\n : query.where('tenant_id', '=', tenantId)\n if (orgScope) {\n query = deps.applyOrganizationScope(query, 'search_tokens.organization_id', orgScope)\n }\n const row = await query.limit(1).executeTakeFirst()\n return !!row\n }\n\n const hasTokens = (\n entity: string,\n tenantId: string | null,\n orgScope?: OrganizationScope | null,\n ): Promise<boolean> => {\n const key = `${entity}|${tenantId ?? '__null__'}|${orgScopeKey(orgScope)}`\n const ttlMs = resolvePresenceCacheTtlMs()\n if (ttlMs > 0) {\n const entry = presenceCache.get(key)\n if (entry && entry.expiresAt > Date.now()) return Promise.resolve(entry.value)\n }\n const cached = tokenPresence.get(key)\n if (cached) return cached\n const probe = (async () => {\n try {\n const value = await probeTokens(entity, tenantId, orgScope)\n // Only genuine probe results enter the process-level cache \u2014 caching an\n // error-driven `false` would pin degraded search for a full TTL after a\n // transient DB failure.\n if (ttlMs > 0) storePresence(key, value, ttlMs)\n return value\n } catch (err) {\n deps.logDebug('search:has-tokens-error', {\n entity,\n tenantId,\n organizationScope: orgScope,\n error: err instanceof Error ? err.message : String(err),\n })\n return false\n }\n })()\n tokenPresence.set(key, probe)\n return probe\n }\n\n return {\n staticEnabled: async () => deps.getConfig().enabled && await tableExists('search_tokens'),\n hasTokens,\n anySourceHasTokens: async (sources, tenantId, orgScope) => {\n for (const source of sources) {\n const ok = await hasTokens(source.entity, tenantId, orgScope)\n deps.logDebug('search:source-has-tokens', {\n entity: source.entity,\n recordIdColumn: source.recordIdColumn,\n tenantId,\n organizationScope: orgScope,\n hasTokens: ok,\n })\n if (ok) return true\n }\n return false\n },\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,WAAW;AACpB,SAAS,8BAA8B;AAoEhC,SAAS,iBAAiB,IAAwC;AACvE,SAAO,OAAO,UAAU,OAAO;AACjC;AAQO,SAAS,gBAAgB,SAAyD;AACvF,SAAO,QAAQ,KAAK,CAAC,WAAW,iBAAiB,OAAO,EAAE,CAAC;AAC7D;AAEA,SAAS,YAAY,OAAqD;AACxE,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,GAAG,MAAM,cAAc,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC,EAAE,KAAK,GAAG,CAAC;AACtH;AAEA,MAAM,gCAAgC;AACtC,MAAM,6BAA6B;AAWnC,MAAM,gBAAgB,oBAAI,IAAmD;AAE7E,SAAS,4BAAoC;AAC3C,SAAO,uBAAuB,QAAQ,IAAI,mCAAmC,+BAA+B,EAAE,SAAS,MAAM,KAAK,EAAE,CAAC;AACvI;AAEA,SAAS,cAAc,KAAa,OAAgB,OAAqB;AACvE,MAAI,cAAc,QAAQ,4BAA4B;AACpD,UAAM,MAAM,KAAK,IAAI;AACrB,eAAW,CAAC,UAAU,KAAK,KAAK,eAAe;AAC7C,UAAI,MAAM,aAAa,IAAK,eAAc,OAAO,QAAQ;AAAA,IAC3D;AACA,QAAI,cAAc,QAAQ,2BAA4B,eAAc,MAAM;AAAA,EAC5E;AACA,gBAAc,IAAI,KAAK,EAAE,OAAO,WAAW,KAAK,IAAI,IAAI,MAAM,CAAC;AACjE;AAEO,SAAS,gCAAsC;AACpD,gBAAc,MAAM;AACtB;AAaO,SAAS,8BAA8B,MAA4D;AACxG,QAAM,gBAAgB,oBAAI,IAA8B;AACxD,QAAM,gBAAgB,oBAAI,IAA8B;AAExD,QAAM,cAAc,CAAC,UAAoC;AACvD,UAAM,SAAS,cAAc,IAAI,KAAK;AACtC,QAAI,OAAQ,QAAO;AACnB,UAAM,SAAS,YAAY;AACzB,YAAM,MAAM,MAAM,KAAK,MAAM,EAC1B,WAAW,2BAA2B,EACtC,OAAO,OAAe,GAAG,KAAK,CAAC,EAC/B,MAAM,cAAc,KAAK,KAAK,EAC9B,MAAM,CAAC,EACP,iBAAiB;AACpB,aAAO,CAAC,CAAC;AAAA,IACX,GAAG;AACH,kBAAc,IAAI,OAAO,KAAK;AAC9B,UAAM,MAAM,MAAM,cAAc,OAAO,KAAK,CAAC;AAC7C,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,OAClB,QACA,UACA,aACqB;AACrB,QAAI,QAAQ,KAAK,MAAM,EACpB,WAAW,eAAe,EAC1B,OAAO,OAAe,GAAG,KAAK,CAAC,EAC/B,MAAM,eAAe,KAAK,MAAM;AAOnC,YAAQ,YAAY,OAChB,MAAM,MAAM,aAAa,MAAM,IAAI,IACnC,MAAM,MAAM,aAAa,KAAK,QAAQ;AAC1C,QAAI,UAAU;AACZ,cAAQ,KAAK,uBAAuB,OAAO,iCAAiC,QAAQ;AAAA,IACtF;AACA,UAAM,MAAM,MAAM,MAAM,MAAM,CAAC,EAAE,iBAAiB;AAClD,WAAO,CAAC,CAAC;AAAA,EACX;AAEA,QAAM,YAAY,CAChB,QACA,UACA,aACqB;AACrB,UAAM,MAAM,GAAG,MAAM,IAAI,YAAY,UAAU,IAAI,YAAY,QAAQ,CAAC;AACxE,UAAM,QAAQ,0BAA0B;AACxC,QAAI,QAAQ,GAAG;AACb,YAAM,QAAQ,cAAc,IAAI,GAAG;AACnC,UAAI,SAAS,MAAM,YAAY,KAAK,IAAI,EAAG,QAAO,QAAQ,QAAQ,MAAM,KAAK;AAAA,IAC/E;AACA,UAAM,SAAS,cAAc,IAAI,GAAG;AACpC,QAAI,OAAQ,QAAO;AACnB,UAAM,SAAS,YAAY;AACzB,UAAI;AACF,cAAM,QAAQ,MAAM,YAAY,QAAQ,UAAU,QAAQ;AAI1D,YAAI,QAAQ,EAAG,eAAc,KAAK,OAAO,KAAK;AAC9C,eAAO;AAAA,MACT,SAAS,KAAK;AACZ,aAAK,SAAS,2BAA2B;AAAA,UACvC;AAAA,UACA;AAAA,UACA,mBAAmB;AAAA,UACnB,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,QACxD,CAAC;AACD,eAAO;AAAA,MACT;AAAA,IACF,GAAG;AACH,kBAAc,IAAI,KAAK,KAAK;AAC5B,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,eAAe,YAAY,KAAK,UAAU,EAAE,WAAW,MAAM,YAAY,eAAe;AAAA,IACxF;AAAA,IACA,oBAAoB,OAAO,SAAS,UAAU,aAAa;AACzD,iBAAW,UAAU,SAAS;AAC5B,cAAM,KAAK,MAAM,UAAU,OAAO,QAAQ,UAAU,QAAQ;AAC5D,aAAK,SAAS,4BAA4B;AAAA,UACxC,QAAQ,OAAO;AAAA,UACf,gBAAgB,OAAO;AAAA,UACvB;AAAA,UACA,mBAAmB;AAAA,UACnB,WAAW;AAAA,QACb,CAAC;AACD,YAAI,GAAI,QAAO;AAAA,MACjB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
6
- "names": []
7
- }
@@ -1,46 +0,0 @@
1
- import { sql } from "kysely";
2
- import { resolveSearchConfig } from "./config.js";
3
- import { tokenizeText } from "./tokenize.js";
4
- async function findEntityIdsBySearchTokens({
5
- db,
6
- entityType,
7
- query,
8
- fields,
9
- scope,
10
- config
11
- }) {
12
- const trimmed = query.trim();
13
- if (!trimmed) return { matched: false, reason: "empty-query" };
14
- const searchConfig = config ?? resolveSearchConfig();
15
- if (!searchConfig.enabled) return { matched: false, reason: "search-disabled" };
16
- const { hashes } = tokenizeText(trimmed, searchConfig);
17
- if (!hashes.length) return { matched: false, reason: "no-tokens" };
18
- let builder = db.selectFrom("search_tokens").select("entity_id").where("entity_type", "=", entityType).where("token_hash", "in", hashes);
19
- const scopedFields = (fields ?? []).filter((field) => typeof field === "string" && field.length > 0);
20
- if (scopedFields.length === 1) {
21
- builder = builder.where("field", "=", scopedFields[0]);
22
- } else if (scopedFields.length > 1) {
23
- builder = builder.where("field", "in", Array.from(scopedFields));
24
- }
25
- if (scope?.tenantId !== void 0) {
26
- builder = builder.where(sql`tenant_id is not distinct from ${scope.tenantId}`);
27
- }
28
- if (scope?.organizationId !== void 0) {
29
- builder = scope.organizationId === null ? builder.where(sql`organization_id is not distinct from ${null}`) : builder.where("organization_id", "=", scope.organizationId);
30
- } else if (scope?.organizationIds?.length) {
31
- builder = builder.where("organization_id", "in", Array.from(scope.organizationIds));
32
- }
33
- const rows = await builder.groupBy("entity_id").having(sql`count(distinct token_hash) >= ${hashes.length}`).execute();
34
- const ids = rows.map((row) => typeof row.entity_id === "string" ? row.entity_id : null).filter((id) => typeof id === "string" && id.length > 0);
35
- return { matched: true, ids };
36
- }
37
- async function findEntityIdsBySearchTokensCompat(input) {
38
- const result = await findEntityIdsBySearchTokens(input);
39
- if (result.matched) return result.ids;
40
- return result.reason === "empty-query" ? null : [];
41
- }
42
- export {
43
- findEntityIdsBySearchTokens,
44
- findEntityIdsBySearchTokensCompat
45
- };
46
- //# sourceMappingURL=tokenLookup.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/search/tokenLookup.ts"],
4
- "sourcesContent": ["import { type Kysely, sql } from 'kysely'\nimport { resolveSearchConfig, type SearchConfig } from './config'\nimport { tokenizeText } from './tokenize'\n\nexport type SearchTokenDatabase = {\n search_tokens: {\n entity_id: string\n entity_type: string\n field: string\n token_hash: string\n tenant_id: string | null\n organization_id: string | null\n }\n}\n\n/**\n * Tenant/organization scoping for a `search_tokens` lookup.\n *\n * `undefined` and `null` are NOT interchangeable:\n * - `undefined` omits the predicate entirely (the caller owns visibility).\n * - `null` emits a null-safe predicate that matches only globally scoped rows.\n */\nexport type SearchTokenScope = {\n tenantId?: string | null\n organizationId?: string | null\n organizationIds?: readonly string[] | null\n}\n\n/**\n * Why a lookup could not produce an id set. Callers MUST NOT read these as\n * \"nothing matched\" \u2014 the token index was never consulted, so the caller's own\n * predicate (usually an `ilike`) is still the authoritative one.\n */\nexport type SearchTokenLookupSkipReason = 'empty-query' | 'search-disabled' | 'no-tokens'\n\nexport type SearchTokenLookupResult =\n | { matched: true; ids: string[] }\n | { matched: false; reason: SearchTokenLookupSkipReason }\n\nexport type FindEntityIdsBySearchTokensInput = {\n db: Kysely<SearchTokenDatabase>\n entityType: string\n query: string\n fields?: readonly string[] | null\n scope?: SearchTokenScope\n config?: SearchConfig\n}\n\n/**\n * Resolve the record ids whose indexed `search_tokens` cover every token in\n * `query`.\n *\n * This is the encryption-safe replacement for `ilike` filtering on columns an\n * encryption map covers: the stored column holds ciphertext, so\n * `ilike '%term%'` silently matches nothing, while the token index stores\n * hashes of the plaintext and keeps matching. See issue #2990.\n *\n * Matching requires ALL query tokens to be present on the record. The token\n * search strategy in `@open-mercato/search` uses a looser match ratio; list\n * endpoints want the stricter behavior so a two-word query narrows rather than\n * widens the result set.\n */\nexport async function findEntityIdsBySearchTokens({\n db,\n entityType,\n query,\n fields,\n scope,\n config,\n}: FindEntityIdsBySearchTokensInput): Promise<SearchTokenLookupResult> {\n const trimmed = query.trim()\n if (!trimmed) return { matched: false, reason: 'empty-query' }\n\n const searchConfig = config ?? resolveSearchConfig()\n if (!searchConfig.enabled) return { matched: false, reason: 'search-disabled' }\n\n const { hashes } = tokenizeText(trimmed, searchConfig)\n if (!hashes.length) return { matched: false, reason: 'no-tokens' }\n\n let builder = db\n .selectFrom('search_tokens')\n .select('entity_id')\n .where('entity_type', '=', entityType)\n .where('token_hash', 'in', hashes)\n\n const scopedFields = (fields ?? []).filter((field) => typeof field === 'string' && field.length > 0)\n if (scopedFields.length === 1) {\n builder = builder.where('field', '=', scopedFields[0])\n } else if (scopedFields.length > 1) {\n builder = builder.where('field', 'in', Array.from(scopedFields))\n }\n\n if (scope?.tenantId !== undefined) {\n builder = builder.where(sql<boolean>`tenant_id is not distinct from ${scope.tenantId}`)\n }\n\n if (scope?.organizationId !== undefined) {\n builder = scope.organizationId === null\n ? builder.where(sql<boolean>`organization_id is not distinct from ${null}`)\n : builder.where('organization_id', '=', scope.organizationId)\n } else if (scope?.organizationIds?.length) {\n builder = builder.where('organization_id', 'in', Array.from(scope.organizationIds))\n }\n\n const rows = (await builder\n .groupBy('entity_id')\n .having(sql<boolean>`count(distinct token_hash) >= ${hashes.length}`)\n .execute()) as Array<{ entity_id?: unknown }>\n\n const ids = rows\n .map((row) => (typeof row.entity_id === 'string' ? row.entity_id : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n\n return { matched: true, ids }\n}\n\n/**\n * Legacy-shaped adapter for call sites that predate\n * {@link SearchTokenLookupResult}: `null` for a blank query, `[]` for every\n * other non-answer, otherwise the matched ids.\n *\n * Prefer {@link findEntityIdsBySearchTokens} in new code \u2014 the discriminated\n * result distinguishes \"the index says nothing matched\" from \"the index was\n * never consulted\", and that distinction is exactly what a `null`/`[]` pair\n * loses.\n */\nexport async function findEntityIdsBySearchTokensCompat(\n input: FindEntityIdsBySearchTokensInput,\n): Promise<string[] | null> {\n const result = await findEntityIdsBySearchTokens(input)\n if (result.matched) return result.ids\n return result.reason === 'empty-query' ? null : []\n}\n"],
5
- "mappings": "AAAA,SAAsB,WAAW;AACjC,SAAS,2BAA8C;AACvD,SAAS,oBAAoB;AA4D7B,eAAsB,4BAA4B;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuE;AACrE,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO,EAAE,SAAS,OAAO,QAAQ,cAAc;AAE7D,QAAM,eAAe,UAAU,oBAAoB;AACnD,MAAI,CAAC,aAAa,QAAS,QAAO,EAAE,SAAS,OAAO,QAAQ,kBAAkB;AAE9E,QAAM,EAAE,OAAO,IAAI,aAAa,SAAS,YAAY;AACrD,MAAI,CAAC,OAAO,OAAQ,QAAO,EAAE,SAAS,OAAO,QAAQ,YAAY;AAEjE,MAAI,UAAU,GACX,WAAW,eAAe,EAC1B,OAAO,WAAW,EAClB,MAAM,eAAe,KAAK,UAAU,EACpC,MAAM,cAAc,MAAM,MAAM;AAEnC,QAAM,gBAAgB,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC;AACnG,MAAI,aAAa,WAAW,GAAG;AAC7B,cAAU,QAAQ,MAAM,SAAS,KAAK,aAAa,CAAC,CAAC;AAAA,EACvD,WAAW,aAAa,SAAS,GAAG;AAClC,cAAU,QAAQ,MAAM,SAAS,MAAM,MAAM,KAAK,YAAY,CAAC;AAAA,EACjE;AAEA,MAAI,OAAO,aAAa,QAAW;AACjC,cAAU,QAAQ,MAAM,qCAA8C,MAAM,QAAQ,EAAE;AAAA,EACxF;AAEA,MAAI,OAAO,mBAAmB,QAAW;AACvC,cAAU,MAAM,mBAAmB,OAC/B,QAAQ,MAAM,2CAAoD,IAAI,EAAE,IACxE,QAAQ,MAAM,mBAAmB,KAAK,MAAM,cAAc;AAAA,EAChE,WAAW,OAAO,iBAAiB,QAAQ;AACzC,cAAU,QAAQ,MAAM,mBAAmB,MAAM,MAAM,KAAK,MAAM,eAAe,CAAC;AAAA,EACpF;AAEA,QAAM,OAAQ,MAAM,QACjB,QAAQ,WAAW,EACnB,OAAO,oCAA6C,OAAO,MAAM,EAAE,EACnE,QAAQ;AAEX,QAAM,MAAM,KACT,IAAI,CAAC,QAAS,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY,IAAK,EACvE,OAAO,CAAC,OAAqB,OAAO,OAAO,YAAY,GAAG,SAAS,CAAC;AAEvE,SAAO,EAAE,SAAS,MAAM,IAAI;AAC9B;AAYA,eAAsB,kCACpB,OAC0B;AAC1B,QAAM,SAAS,MAAM,4BAA4B,KAAK;AACtD,MAAI,OAAO,QAAS,QAAO,OAAO;AAClC,SAAO,OAAO,WAAW,gBAAgB,OAAO,CAAC;AACnD;",
6
- "names": []
7
- }