@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,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/query/types.ts"],
4
- "sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { Profiler } from '../profiler'\nimport type { ResolvedCustomFieldDefinitions } from '../crud/custom-field-definition-index'\n\nexport type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'ilike' | 'exists'\n\nexport enum SortDir {\n Asc = 'asc',\n Desc = 'desc',\n}\n\nexport type FieldSelector = string // base field or custom field key (prefixed with 'cf:')\n\nexport type Filter = {\n field: FieldSelector\n op: FilterOp\n value?: any\n}\n\nexport type Sort = { field: FieldSelector; dir?: SortDir }\n\nexport type Page = { page?: number; pageSize?: number }\n\n// Mongo/Medusa-style filter operators (typed)\nexport type WhereOps<T> = {\n $eq?: T\n $ne?: T | null\n $gt?: T extends number | Date ? T : never\n $gte?: T extends number | Date ? T : never\n $lt?: T extends number | Date ? T : never\n $lte?: T extends number | Date ? T : never\n $in?: T[]\n $nin?: T[]\n $like?: T extends string ? string : never\n $ilike?: T extends string ? string : never\n $exists?: boolean\n}\n\n// A field filter can be a direct value (equals) or ops object\nexport type WhereValue<T = any> = T | WhereOps<T>\n\n// Generic shape for object filters. If you have a typed map of field\u2192type,\n// pass it as the generic to get end-to-end typing.\n// Example: Where<{\n// id: string; title: string; created_at: Date; 'cf:severity': number\n// }>\nexport type Where<Fields extends Record<string, any> = Record<string, any>> =\n Partial<{ [K in keyof Fields]: WhereValue<Fields[K]> }> & Record<string, WhereValue>\n\nexport type QueryCustomFieldJoin = {\n fromField: string\n toField: string\n type?: 'left' | 'inner'\n}\n\nexport type QueryCustomFieldSource = {\n entityId: EntityId\n table?: string\n alias?: string\n recordIdColumn?: string\n join?: QueryCustomFieldJoin\n tenantField?: string\n organizationField?: string\n}\n\nexport type QueryJoinEdge = {\n alias: string\n table?: string\n entityId?: EntityId\n from: {\n alias?: string\n field: string\n }\n to: {\n field: string\n }\n type?: 'left' | 'inner'\n}\n\n/**\n * Optional context for query-level UMES extensions.\n * When provided, the query engine will execute sync lifecycle events\n * (querying/queried) and apply query-enabled enrichers.\n */\nexport type QueryExtensionsConfig = {\n userId?: string\n container?: unknown\n userFeatures?: string[]\n resolve?: <T = unknown>(name: string) => T\n}\n\nexport type QueryOptions = {\n fields?: FieldSelector[] // base fields and/or 'cf:<key>' for custom fields\n includeExtensions?: boolean | string[] // include all registered extensions or only specific ones by entity id\n includeCustomFields?: boolean | string[] // include all CFs or specific keys\n // Accept classic array syntax or Mongo-style object syntax\n filters?: Filter[] | Where\n sort?: Sort[]\n page?: Page\n organizationId?: string // enforce multi-tenant scope\n tenantId?: string // enforce tenant scope\n // Optional list of organization ids to scope results. Takes precedence over organizationId.\n organizationIds?: string[]\n /**\n * When true, the engine does not apply default `organization_id` / `tenant_id` equality guards.\n *\n * Callers MUST encode full visibility in `filters` (for example with `$or` of scoped branches)\n * and MUST fail closed when the authenticated principal lacks a resolvable tenant/org, otherwise\n * queries return cross-tenant rows.\n *\n * When this flag is set, the hybrid query engine delegates to the basic engine. The basic engine\n * still applies `cf:*` filters/sorts, but `search_tokens` fulltext filtering, the JSONB index read\n * path, and the vector-search branch are BYPASSED. Only use this on entities whose scoping does\n * not match the standard `organization_id = X AND tenant_id = Y` shape.\n */\n omitAutomaticTenantOrgScope?: boolean\n // Soft-delete behavior: when false (default), rows with non-null deleted_at\n // are excluded if the base table has that column. Set true to include them.\n withDeleted?: boolean\n customFieldSources?: QueryCustomFieldSource[]\n joins?: QueryJoinEdge[]\n profiler?: Profiler\n // When true, suppress automatic reindex scheduling triggered by coverage gap detection.\n // Used by the search indexing pipeline to prevent feedback loops where indexing triggers\n // re-indexing indefinitely.\n skipAutoReindex?: boolean\n /**\n * Force routing this query to custom-entity doc storage (`custom_entities_storage`)\n * instead of classifying the entity automatically. Automatic classification routes\n * ids backed by a registered ORM table to that base table, so surfaces that manage\n * doc records for ids that are ALSO table-backed (e.g. the entities records browser\n * reading a module-declared custom entity such as `example:todo`) must set this flag.\n * Honored by the hybrid query engine only; `BasicQueryEngine` has no doc-storage\n * reader and ignores it.\n */\n forceCustomEntityStorage?: boolean\n // Optional UMES query extensions context. When provided, the engine will\n // emit sync lifecycle events and apply query-level enrichers.\n extensions?: QueryExtensionsConfig\n}\n\nexport type PartialIndexWarning = {\n entity: EntityId\n entityLabel?: string | null\n baseCount?: number | null\n indexedCount?: number | null\n scope?: 'scoped' | 'global'\n}\n\nexport type EncryptedSortRowCapWarning = {\n entity: EntityId\n sortFields: string[]\n maxRows: number\n totalMatched: number\n}\n\nexport type QueryResultMeta = {\n partialIndexWarning?: PartialIndexWarning\n encryptedSortRowCapWarning?: EncryptedSortRowCapWarning\n}\n\nexport type QueryResult<T = any> = {\n items: T[]\n page: number\n pageSize: number\n total: number\n meta?: QueryResultMeta\n /**\n * Custom-field definitions the engine resolved while building this result\n * (only present when `includeCustomFields: true`). Lets the CRUD factory\n * decorate list rows without reloading definitions from the DB (issue #2133).\n * Internal contract \u2014 additive and optional; callers must treat absence as a\n * cue to load definitions themselves.\n */\n customFieldDefinitions?: ResolvedCustomFieldDefinitions\n}\n\nexport interface QueryEngine {\n query<T = any>(entity: EntityId, opts?: QueryOptions): Promise<QueryResult<T>>\n}\n"],
4
+ "sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { Profiler } from '../profiler'\nimport type { ResolvedCustomFieldDefinitions } from '../crud/custom-field-definition-index'\n\nexport type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'ilike' | 'exists'\n\nexport enum SortDir {\n Asc = 'asc',\n Desc = 'desc',\n}\n\nexport type FieldSelector = string // base field or custom field key (prefixed with 'cf:')\n\nexport type Filter = {\n field: FieldSelector\n op: FilterOp\n value?: any\n}\n\nexport type Sort = { field: FieldSelector; dir?: SortDir }\n\nexport type Page = { page?: number; pageSize?: number }\n\n// Mongo/Medusa-style filter operators (typed)\nexport type WhereOps<T> = {\n $eq?: T\n $ne?: T | null\n $gt?: T extends number | Date ? T : never\n $gte?: T extends number | Date ? T : never\n $lt?: T extends number | Date ? T : never\n $lte?: T extends number | Date ? T : never\n $in?: T[]\n $nin?: T[]\n $like?: T extends string ? string : never\n $ilike?: T extends string ? string : never\n $exists?: boolean\n}\n\n// A field filter can be a direct value (equals) or ops object\nexport type WhereValue<T = any> = T | WhereOps<T>\n\n// Generic shape for object filters. If you have a typed map of field\u2192type,\n// pass it as the generic to get end-to-end typing.\n// Example: Where<{\n// id: string; title: string; created_at: Date; 'cf:severity': number\n// }>\nexport type Where<Fields extends Record<string, any> = Record<string, any>> =\n Partial<{ [K in keyof Fields]: WhereValue<Fields[K]> }> & Record<string, WhereValue>\n\nexport type QueryCustomFieldJoin = {\n fromField: string\n toField: string\n type?: 'left' | 'inner'\n}\n\nexport type QueryCustomFieldSource = {\n entityId: EntityId\n table?: string\n alias?: string\n recordIdColumn?: string\n join?: QueryCustomFieldJoin\n tenantField?: string\n organizationField?: string\n}\n\nexport type QueryJoinEdge = {\n alias: string\n table?: string\n entityId?: EntityId\n from: {\n alias?: string\n field: string\n }\n to: {\n field: string\n }\n type?: 'left' | 'inner'\n}\n\n/**\n * Optional context for query-level UMES extensions.\n * When provided, the query engine will execute sync lifecycle events\n * (querying/queried) and apply query-enabled enrichers.\n */\nexport type QueryExtensionsConfig = {\n userId?: string\n container?: unknown\n userFeatures?: string[]\n resolve?: <T = unknown>(name: string) => T\n}\n\nexport type QueryOptions = {\n fields?: FieldSelector[] // base fields and/or 'cf:<key>' for custom fields\n includeExtensions?: boolean | string[] // include all registered extensions or only specific ones by entity id\n includeCustomFields?: boolean | string[] // include all CFs or specific keys\n // Accept classic array syntax or Mongo-style object syntax\n filters?: Filter[] | Where\n sort?: Sort[]\n page?: Page\n organizationId?: string // enforce multi-tenant scope\n tenantId?: string // enforce tenant scope\n // Optional list of organization ids to scope results. Takes precedence over organizationId.\n organizationIds?: string[]\n /**\n * When true, the engine does not apply default `organization_id` / `tenant_id` equality guards.\n *\n * Callers MUST encode full visibility in `filters` (for example with `$or` of scoped branches)\n * and MUST fail closed when the authenticated principal lacks a resolvable tenant/org, otherwise\n * queries return cross-tenant rows.\n *\n * When this flag is set, the hybrid query engine delegates to the basic engine, which means\n * custom-field (`cf:*`) filters/sorts, `search_tokens` fulltext filtering, and vector-search\n * branches are BYPASSED. Only use this on entities whose scoping does not match the standard\n * `organization_id = X AND tenant_id = Y` shape and which do not rely on custom-field/search\n * features.\n */\n omitAutomaticTenantOrgScope?: boolean\n // Soft-delete behavior: when false (default), rows with non-null deleted_at\n // are excluded if the base table has that column. Set true to include them.\n withDeleted?: boolean\n customFieldSources?: QueryCustomFieldSource[]\n joins?: QueryJoinEdge[]\n profiler?: Profiler\n // When true, suppress automatic reindex scheduling triggered by coverage gap detection.\n // Used by the search indexing pipeline to prevent feedback loops where indexing triggers\n // re-indexing indefinitely.\n skipAutoReindex?: boolean\n /**\n * Force routing this query to custom-entity doc storage (`custom_entities_storage`)\n * instead of classifying the entity automatically. Automatic classification routes\n * ids backed by a registered ORM table to that base table, so surfaces that manage\n * doc records for ids that are ALSO table-backed (e.g. the entities records browser\n * reading a module-declared custom entity such as `example:todo`) must set this flag.\n * Honored by the hybrid query engine only; `BasicQueryEngine` has no doc-storage\n * reader and ignores it.\n */\n forceCustomEntityStorage?: boolean\n // Optional UMES query extensions context. When provided, the engine will\n // emit sync lifecycle events and apply query-level enrichers.\n extensions?: QueryExtensionsConfig\n}\n\nexport type PartialIndexWarning = {\n entity: EntityId\n entityLabel?: string | null\n baseCount?: number | null\n indexedCount?: number | null\n scope?: 'scoped' | 'global'\n}\n\nexport type EncryptedSortRowCapWarning = {\n entity: EntityId\n sortFields: string[]\n maxRows: number\n totalMatched: number\n}\n\nexport type QueryResultMeta = {\n partialIndexWarning?: PartialIndexWarning\n encryptedSortRowCapWarning?: EncryptedSortRowCapWarning\n}\n\nexport type QueryResult<T = any> = {\n items: T[]\n page: number\n pageSize: number\n total: number\n meta?: QueryResultMeta\n /**\n * Custom-field definitions the engine resolved while building this result\n * (only present when `includeCustomFields: true`). Lets the CRUD factory\n * decorate list rows without reloading definitions from the DB (issue #2133).\n * Internal contract \u2014 additive and optional; callers must treat absence as a\n * cue to load definitions themselves.\n */\n customFieldDefinitions?: ResolvedCustomFieldDefinitions\n}\n\nexport interface QueryEngine {\n query<T = any>(entity: EntityId, opts?: QueryOptions): Promise<QueryResult<T>>\n}\n"],
5
5
  "mappings": "AAMO,IAAK,UAAL,kBAAKA,aAAL;AACL,EAAAA,SAAA,SAAM;AACN,EAAAA,SAAA,UAAO;AAFG,SAAAA;AAAA,GAAA;",
6
6
  "names": ["SortDir"]
7
7
  }
@@ -21,17 +21,12 @@ function parseRedisUrl(url) {
21
21
  const dbStr = parsed.pathname ? parsed.pathname.slice(1) : "";
22
22
  const dbParsed = dbStr !== "" ? parseInt(dbStr, 10) : NaN;
23
23
  const db = Number.isNaN(dbParsed) ? void 0 : dbParsed;
24
- const familyValue = parsed.searchParams.get("family");
25
- const familyParsed = familyValue !== null ? parseInt(familyValue, 10) : NaN;
26
- const family = [0, 4, 6].includes(familyParsed) ? familyParsed : void 0;
27
24
  return {
28
25
  host: parsed.hostname || "localhost",
29
26
  port: parseInt(parsed.port, 10) || 6379,
30
- username: parsed.username ? decodeURIComponent(parsed.username) : void 0,
31
- password: parsed.password ? decodeURIComponent(parsed.password) : void 0,
27
+ password: parsed.password || void 0,
32
28
  db,
33
- tls: parsed.protocol === "rediss:" ? {} : void 0,
34
- family
29
+ tls: parsed.protocol === "rediss:" ? {} : void 0
35
30
  };
36
31
  } catch {
37
32
  const safeUrl = url.replace(/\/\/[^:]*:[^@]*@/, "//<redacted>@");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/redis/connection.ts"],
4
- "sourcesContent": ["import { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'redis' })\n\n/**\n * Shared Redis connection utilities.\n *\n * Every package that needs a Redis URL or parsed connection options\n * should import from here instead of reading env vars directly.\n *\n * The `prefix` parameter lets each subsystem define its own override:\n * getRedisUrl('QUEUE') \u2192 QUEUE_REDIS_URL > REDIS_URL > null\n * getRedisUrl('CACHE') \u2192 CACHE_REDIS_URL > REDIS_URL > null\n * getRedisUrl() \u2192 REDIS_URL > null\n *\n * Returns null when Redis is not configured so callers can explicitly\n * decide whether to fall back to a non-Redis strategy or fail loudly.\n * Use getRedisUrlOrThrow() when Redis is mandatory for the caller.\n */\n\nexport type ParsedRedisConnection = {\n host: string\n port: number\n username?: string\n password?: string\n db?: number\n tls?: Record<string, unknown>\n family?: number\n}\n\n/**\n * Resolve a Redis URL from environment variables.\n *\n * Priority: <PREFIX>_REDIS_URL \u2192 REDIS_URL \u2192 null\n *\n * Returns null when no env var is set. Callers MUST NOT assume a\n * localhost default \u2014 silently connecting to localhost masks missing\n * configuration and stalls on platforms where nothing listens there\n * (e.g., WSL2 without a local Redis).\n */\nexport function getRedisUrl(prefix?: string): string | null {\n if (prefix) {\n const prefixed = process.env[`${prefix}_REDIS_URL`]\n if (prefixed) return prefixed\n }\n return process.env.REDIS_URL || null\n}\n\n/**\n * Like getRedisUrl, but throws a descriptive error when Redis is not\n * configured. Use from code paths that require Redis (e.g. BullMQ\n * async queue, Redis cache strategy, scheduler service).\n */\nexport function getRedisUrlOrThrow(prefix?: string): string {\n const url = getRedisUrl(prefix)\n if (url) return url\n const which = prefix ? `${prefix}_REDIS_URL or REDIS_URL` : 'REDIS_URL'\n throw new Error(\n `Redis URL is not configured. Set ${which} in your environment to use a Redis-backed strategy.`\n )\n}\n\n/**\n * Parse a redis:// URL into structured connection options suitable for\n * BullMQ and ioredis.\n */\nexport function parseRedisUrl(url: string): ParsedRedisConnection {\n try {\n const parsed = new URL(url)\n const dbStr = parsed.pathname ? parsed.pathname.slice(1) : ''\n const dbParsed = dbStr !== '' ? parseInt(dbStr, 10) : NaN\n const db = Number.isNaN(dbParsed) ? undefined : dbParsed\n const familyValue = parsed.searchParams.get('family')\n const familyParsed = familyValue !== null ? parseInt(familyValue, 10) : NaN\n const family = [0, 4, 6].includes(familyParsed) ? familyParsed : undefined\n return {\n host: parsed.hostname || 'localhost',\n port: parseInt(parsed.port, 10) || 6379,\n username: parsed.username ? decodeURIComponent(parsed.username) : undefined,\n password: parsed.password ? decodeURIComponent(parsed.password) : undefined,\n db,\n tls: parsed.protocol === 'rediss:' ? {} : undefined,\n family,\n }\n } catch {\n const safeUrl = url.replace(/\\/\\/[^:]*:[^@]*@/, '//<redacted>@')\n logger.warn('Failed to parse Redis URL, falling back to localhost:6379', { url: safeUrl })\n return { host: 'localhost', port: 6379 }\n }\n}\n\n/**\n * Convenience: resolve the URL from env and parse it in one step.\n * Returns null when Redis is not configured.\n */\nexport function resolveRedisConnection(\n prefix?: string,\n): (ParsedRedisConnection & { url: string }) | null {\n const url = getRedisUrl(prefix)\n if (!url) return null\n return { url, ...parseRedisUrl(url) }\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAsC3D,SAAS,YAAY,QAAgC;AAC1D,MAAI,QAAQ;AACV,UAAM,WAAW,QAAQ,IAAI,GAAG,MAAM,YAAY;AAClD,QAAI,SAAU,QAAO;AAAA,EACvB;AACA,SAAO,QAAQ,IAAI,aAAa;AAClC;AAOO,SAAS,mBAAmB,QAAyB;AAC1D,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,IAAK,QAAO;AAChB,QAAM,QAAQ,SAAS,GAAG,MAAM,4BAA4B;AAC5D,QAAM,IAAI;AAAA,IACR,oCAAoC,KAAK;AAAA,EAC3C;AACF;AAMO,SAAS,cAAc,KAAoC;AAChE,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAM,QAAQ,OAAO,WAAW,OAAO,SAAS,MAAM,CAAC,IAAI;AAC3D,UAAM,WAAW,UAAU,KAAK,SAAS,OAAO,EAAE,IAAI;AACtD,UAAM,KAAK,OAAO,MAAM,QAAQ,IAAI,SAAY;AAChD,UAAM,cAAc,OAAO,aAAa,IAAI,QAAQ;AACpD,UAAM,eAAe,gBAAgB,OAAO,SAAS,aAAa,EAAE,IAAI;AACxE,UAAM,SAAS,CAAC,GAAG,GAAG,CAAC,EAAE,SAAS,YAAY,IAAI,eAAe;AACjE,WAAO;AAAA,MACL,MAAM,OAAO,YAAY;AAAA,MACzB,MAAM,SAAS,OAAO,MAAM,EAAE,KAAK;AAAA,MACnC,UAAU,OAAO,WAAW,mBAAmB,OAAO,QAAQ,IAAI;AAAA,MAClE,UAAU,OAAO,WAAW,mBAAmB,OAAO,QAAQ,IAAI;AAAA,MAClE;AAAA,MACA,KAAK,OAAO,aAAa,YAAY,CAAC,IAAI;AAAA,MAC1C;AAAA,IACF;AAAA,EACF,QAAQ;AACN,UAAM,UAAU,IAAI,QAAQ,oBAAoB,eAAe;AAC/D,WAAO,KAAK,6DAA6D,EAAE,KAAK,QAAQ,CAAC;AACzF,WAAO,EAAE,MAAM,aAAa,MAAM,KAAK;AAAA,EACzC;AACF;AAMO,SAAS,uBACd,QACkD;AAClD,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,EAAE,KAAK,GAAG,cAAc,GAAG,EAAE;AACtC;",
4
+ "sourcesContent": ["import { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'redis' })\n\n/**\n * Shared Redis connection utilities.\n *\n * Every package that needs a Redis URL or parsed connection options\n * should import from here instead of reading env vars directly.\n *\n * The `prefix` parameter lets each subsystem define its own override:\n * getRedisUrl('QUEUE') \u2192 QUEUE_REDIS_URL > REDIS_URL > null\n * getRedisUrl('CACHE') \u2192 CACHE_REDIS_URL > REDIS_URL > null\n * getRedisUrl() \u2192 REDIS_URL > null\n *\n * Returns null when Redis is not configured so callers can explicitly\n * decide whether to fall back to a non-Redis strategy or fail loudly.\n * Use getRedisUrlOrThrow() when Redis is mandatory for the caller.\n */\n\nexport type ParsedRedisConnection = {\n host: string\n port: number\n password?: string\n db?: number\n tls?: Record<string, unknown>\n}\n\n/**\n * Resolve a Redis URL from environment variables.\n *\n * Priority: <PREFIX>_REDIS_URL \u2192 REDIS_URL \u2192 null\n *\n * Returns null when no env var is set. Callers MUST NOT assume a\n * localhost default \u2014 silently connecting to localhost masks missing\n * configuration and stalls on platforms where nothing listens there\n * (e.g., WSL2 without a local Redis).\n */\nexport function getRedisUrl(prefix?: string): string | null {\n if (prefix) {\n const prefixed = process.env[`${prefix}_REDIS_URL`]\n if (prefixed) return prefixed\n }\n return process.env.REDIS_URL || null\n}\n\n/**\n * Like getRedisUrl, but throws a descriptive error when Redis is not\n * configured. Use from code paths that require Redis (e.g. BullMQ\n * async queue, Redis cache strategy, scheduler service).\n */\nexport function getRedisUrlOrThrow(prefix?: string): string {\n const url = getRedisUrl(prefix)\n if (url) return url\n const which = prefix ? `${prefix}_REDIS_URL or REDIS_URL` : 'REDIS_URL'\n throw new Error(\n `Redis URL is not configured. Set ${which} in your environment to use a Redis-backed strategy.`\n )\n}\n\n/**\n * Parse a redis:// URL into a {host, port, password, db} object\n * suitable for BullMQ / ioredis structured connection options.\n *\n * @deprecated Prefer passing the full URL via `{ url: getRedisUrl(...) }` to\n * BullMQ/ioredis \u2014 this preserves rediss://, username, database, and query\n * params that structured parsing may lose. Kept for backward compatibility.\n */\nexport function parseRedisUrl(url: string): ParsedRedisConnection {\n try {\n const parsed = new URL(url)\n const dbStr = parsed.pathname ? parsed.pathname.slice(1) : ''\n const dbParsed = dbStr !== '' ? parseInt(dbStr, 10) : NaN\n const db = Number.isNaN(dbParsed) ? undefined : dbParsed\n return {\n host: parsed.hostname || 'localhost',\n port: parseInt(parsed.port, 10) || 6379,\n password: parsed.password || undefined,\n db,\n tls: parsed.protocol === 'rediss:' ? {} : undefined,\n }\n } catch {\n const safeUrl = url.replace(/\\/\\/[^:]*:[^@]*@/, '//<redacted>@')\n logger.warn('Failed to parse Redis URL, falling back to localhost:6379', { url: safeUrl })\n return { host: 'localhost', port: 6379 }\n }\n}\n\n/**\n * Convenience: resolve the URL from env and parse it in one step.\n * Returns null when Redis is not configured.\n */\nexport function resolveRedisConnection(\n prefix?: string,\n): (ParsedRedisConnection & { url: string }) | null {\n const url = getRedisUrl(prefix)\n if (!url) return null\n return { url, ...parseRedisUrl(url) }\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAoC3D,SAAS,YAAY,QAAgC;AAC1D,MAAI,QAAQ;AACV,UAAM,WAAW,QAAQ,IAAI,GAAG,MAAM,YAAY;AAClD,QAAI,SAAU,QAAO;AAAA,EACvB;AACA,SAAO,QAAQ,IAAI,aAAa;AAClC;AAOO,SAAS,mBAAmB,QAAyB;AAC1D,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,IAAK,QAAO;AAChB,QAAM,QAAQ,SAAS,GAAG,MAAM,4BAA4B;AAC5D,QAAM,IAAI;AAAA,IACR,oCAAoC,KAAK;AAAA,EAC3C;AACF;AAUO,SAAS,cAAc,KAAoC;AAChE,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAM,QAAQ,OAAO,WAAW,OAAO,SAAS,MAAM,CAAC,IAAI;AAC3D,UAAM,WAAW,UAAU,KAAK,SAAS,OAAO,EAAE,IAAI;AACtD,UAAM,KAAK,OAAO,MAAM,QAAQ,IAAI,SAAY;AAChD,WAAO;AAAA,MACL,MAAM,OAAO,YAAY;AAAA,MACzB,MAAM,SAAS,OAAO,MAAM,EAAE,KAAK;AAAA,MACnC,UAAU,OAAO,YAAY;AAAA,MAC7B;AAAA,MACA,KAAK,OAAO,aAAa,YAAY,CAAC,IAAI;AAAA,IAC5C;AAAA,EACF,QAAQ;AACN,UAAM,UAAU,IAAI,QAAQ,oBAAoB,eAAe;AAC/D,WAAO,KAAK,6DAA6D,EAAE,KAAK,QAAQ,CAAC;AACzF,WAAO,EAAE,MAAM,aAAa,MAAM,KAAK;AAAA,EACzC;AACF;AAMO,SAAS,uBACd,QACkD;AAClD,QAAM,MAAM,YAAY,MAAM;AAC9B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,EAAE,KAAK,GAAG,cAAc,GAAG,EAAE;AACtC;",
6
6
  "names": []
7
7
  }
@@ -1,94 +1,35 @@
1
1
  import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
2
2
  import { parseNumberWithDefault } from "@open-mercato/shared/lib/number";
3
- import { parseCommaSeparatedList } from "@open-mercato/shared/lib/string";
4
3
  const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3;
5
- const DEFAULT_SEARCH_MAX_FIELD_CHARS = 2e4;
6
- const DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD = 5e3;
7
- const DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD = 2e4;
8
4
  const DEFAULT_BLOCKLIST = ["password", "token", "secret", "hash"];
9
- const ENTITY_BLOCKLIST_SEPARATOR = "@";
10
5
  function parseBoolean(raw, fallback) {
11
6
  return parseBooleanWithDefault(raw, fallback);
12
7
  }
13
8
  function parseNumber(raw, fallback, min = 1) {
14
9
  return parseNumberWithDefault(raw, fallback, { integer: true, min });
15
10
  }
16
- function resolveSearchTokenLimits(config) {
17
- const resolveLimit = (value, fallback) => {
18
- if (value === void 0) return fallback;
19
- if (!Number.isFinite(value) || value < 0) return fallback;
20
- return Math.trunc(value);
21
- };
22
- return {
23
- maxFieldChars: resolveLimit(config.maxFieldChars, DEFAULT_SEARCH_MAX_FIELD_CHARS),
24
- maxTokensPerField: resolveLimit(config.maxTokensPerField, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD),
25
- maxTokensPerRecord: resolveLimit(config.maxTokensPerRecord, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD)
26
- };
27
- }
28
11
  function parseHashAlgorithm(raw) {
29
12
  const value = (raw ?? "").trim().toLowerCase();
30
13
  if (value === "sha1") return "sha1";
31
14
  if (value === "md5") return "md5";
32
15
  return "sha256";
33
16
  }
34
- function parseFieldBlocklist(raw) {
35
- const global = [];
36
- const byEntity = /* @__PURE__ */ new Map();
37
- for (const rawEntry of parseCommaSeparatedList(raw)) {
38
- const entry = rawEntry.toLowerCase();
39
- const separatorIndex = entry.indexOf(ENTITY_BLOCKLIST_SEPARATOR);
40
- const entityType = separatorIndex >= 0 ? entry.slice(0, separatorIndex).trim() : "";
41
- const field = separatorIndex >= 0 ? entry.slice(separatorIndex + 1).trim() : entry;
42
- if (!field.length) continue;
43
- if (!entityType.length) {
44
- if (!global.includes(field)) global.push(field);
45
- continue;
46
- }
47
- const scoped = byEntity.get(entityType) ?? [];
48
- if (!scoped.includes(field)) scoped.push(field);
49
- byEntity.set(entityType, scoped);
50
- }
51
- for (const fallback of DEFAULT_BLOCKLIST) {
52
- if (!global.includes(fallback)) global.push(fallback);
53
- }
54
- const scopedBlocklist = /* @__PURE__ */ Object.create(null);
55
- for (const [entityType, fields] of byEntity) scopedBlocklist[entityType] = fields;
56
- return { global, byEntity: scopedBlocklist };
57
- }
58
17
  function resolveSearchConfig() {
59
- const blocklist = parseFieldBlocklist(process.env.OM_SEARCH_FIELD_BLOCKLIST);
60
18
  return {
61
19
  enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),
62
20
  minTokenLength: resolveSearchMinTokenLength(),
63
21
  enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),
64
22
  hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),
65
23
  storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),
66
- blocklistedFields: blocklist.global,
67
- entityBlocklistedFields: blocklist.byEntity,
68
- maxFieldChars: parseNumber(process.env.OM_SEARCH_MAX_FIELD_CHARS, DEFAULT_SEARCH_MAX_FIELD_CHARS, 0),
69
- maxTokensPerField: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_FIELD, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD, 0),
70
- maxTokensPerRecord: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_RECORD, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD, 0)
24
+ blocklistedFields: (process.env.OM_SEARCH_FIELD_BLOCKLIST ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).filter((value, index, arr) => arr.indexOf(value) === index).map((entry) => entry.toLowerCase()).concat(DEFAULT_BLOCKLIST).filter((value, index, arr) => arr.indexOf(value) === index)
71
25
  };
72
26
  }
73
- function isSearchFieldBlocklisted(field, entityType, config) {
74
- const lower = field.toLowerCase();
75
- if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return true;
76
- if (!entityType) return false;
77
- const scoped = config.entityBlocklistedFields?.[entityType.trim().toLowerCase()];
78
- if (!Array.isArray(scoped) || !scoped.length) return false;
79
- return scoped.some((blocked) => lower.includes(blocked));
80
- }
81
27
  function resolveSearchMinTokenLength() {
82
28
  return parseNumber(process.env.OM_SEARCH_MIN_LEN, DEFAULT_SEARCH_MIN_TOKEN_LENGTH, 1);
83
29
  }
84
30
  export {
85
- DEFAULT_SEARCH_MAX_FIELD_CHARS,
86
- DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD,
87
- DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD,
88
31
  DEFAULT_SEARCH_MIN_TOKEN_LENGTH,
89
- isSearchFieldBlocklisted,
90
32
  resolveSearchConfig,
91
- resolveSearchMinTokenLength,
92
- resolveSearchTokenLimits
33
+ resolveSearchMinTokenLength
93
34
  };
94
35
  //# sourceMappingURL=config.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/search/config.ts"],
4
- "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseNumberWithDefault } from '@open-mercato/shared/lib/number'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\n\nexport type SearchConfig = {\n enabled: boolean\n minTokenLength: number\n enablePartials: boolean\n hashAlgorithm: 'sha256' | 'sha1' | 'md5'\n storeRawTokens: boolean\n blocklistedFields: string[]\n entityBlocklistedFields?: Record<string, string[]>\n maxFieldChars?: number\n maxTokensPerField?: number\n maxTokensPerRecord?: number\n}\n\nexport const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3\nexport const DEFAULT_SEARCH_MAX_FIELD_CHARS = 20_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD = 5_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD = 20_000\n\nexport type SearchTokenLimits = {\n maxFieldChars: number\n maxTokensPerField: number\n maxTokensPerRecord: number\n}\n\nconst DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']\n\nconst ENTITY_BLOCKLIST_SEPARATOR = '@'\n\nfunction parseBoolean(raw: string | undefined, fallback: boolean): boolean {\n return parseBooleanWithDefault(raw, fallback)\n}\n\nfunction parseNumber(raw: string | undefined, fallback: number, min = 1): number {\n return parseNumberWithDefault(raw, fallback, { integer: true, min })\n}\n\nexport function resolveSearchTokenLimits(config: SearchConfig): SearchTokenLimits {\n const resolveLimit = (value: number | undefined, fallback: number): number => {\n if (value === undefined) return fallback\n if (!Number.isFinite(value) || value < 0) return fallback\n return Math.trunc(value)\n }\n return {\n maxFieldChars: resolveLimit(config.maxFieldChars, DEFAULT_SEARCH_MAX_FIELD_CHARS),\n maxTokensPerField: resolveLimit(config.maxTokensPerField, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD),\n maxTokensPerRecord: resolveLimit(config.maxTokensPerRecord, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD),\n }\n}\n\nfunction parseHashAlgorithm(raw: string | undefined): 'sha256' | 'sha1' | 'md5' {\n const value = (raw ?? '').trim().toLowerCase()\n if (value === 'sha1') return 'sha1'\n if (value === 'md5') return 'md5'\n return 'sha256'\n}\n\n/**\n * Parses `OM_SEARCH_FIELD_BLOCKLIST` into a global list plus per-entity-type lists.\n *\n * Why: a deployment often needs to keep one large free-text column out of the token\n * index (e-mail bodies on `customers:customer_interaction`) while still indexing the\n * same-named column elsewhere. A flat global list cannot express that.\n *\n * How to apply: entries are comma-separated; an entry may carry an optional\n * `entityType@` prefix \u2014 `body` blocks the field everywhere, while\n * `customers:customer_interaction@body` blocks it only for that entity type. Entries\n * whose field part is empty are ignored so malformed env input cannot break indexing.\n */\nfunction parseFieldBlocklist(raw: string | undefined): {\n global: string[]\n byEntity: Record<string, string[]>\n} {\n const global: string[] = []\n const byEntity = new Map<string, string[]>()\n\n for (const rawEntry of parseCommaSeparatedList(raw)) {\n const entry = rawEntry.toLowerCase()\n const separatorIndex = entry.indexOf(ENTITY_BLOCKLIST_SEPARATOR)\n const entityType = separatorIndex >= 0 ? entry.slice(0, separatorIndex).trim() : ''\n const field = separatorIndex >= 0 ? entry.slice(separatorIndex + 1).trim() : entry\n if (!field.length) continue\n\n if (!entityType.length) {\n if (!global.includes(field)) global.push(field)\n continue\n }\n\n const scoped = byEntity.get(entityType) ?? []\n if (!scoped.includes(field)) scoped.push(field)\n byEntity.set(entityType, scoped)\n }\n\n for (const fallback of DEFAULT_BLOCKLIST) {\n if (!global.includes(fallback)) global.push(fallback)\n }\n\n const scopedBlocklist = Object.create(null) as Record<string, string[]>\n for (const [entityType, fields] of byEntity) scopedBlocklist[entityType] = fields\n\n return { global, byEntity: scopedBlocklist }\n}\n\nexport function resolveSearchConfig(): SearchConfig {\n const blocklist = parseFieldBlocklist(process.env.OM_SEARCH_FIELD_BLOCKLIST)\n return {\n enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),\n minTokenLength: resolveSearchMinTokenLength(),\n enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),\n hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),\n storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),\n blocklistedFields: blocklist.global,\n entityBlocklistedFields: blocklist.byEntity,\n maxFieldChars: parseNumber(process.env.OM_SEARCH_MAX_FIELD_CHARS, DEFAULT_SEARCH_MAX_FIELD_CHARS, 0),\n maxTokensPerField: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_FIELD, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD, 0),\n maxTokensPerRecord: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_RECORD, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD, 0),\n }\n}\n\n/**\n * Single matcher for \"should this field be kept out of the search index?\".\n *\n * Why: the per-field token path and the `search_text` aggregate previously each\n * decided this on their own, and the aggregate simply never consulted the config \u2014\n * so a blocklisted column's text came back into the index under the aggregate's\n * field name (#4624). Both paths now share this function so they cannot drift.\n *\n * How to apply: pass the document's field name and the entity type being indexed;\n * `entityType` may be omitted when unknown, in which case only global entries apply.\n * Matching keeps the historical substring semantics (`fieldName.includes(pattern)`).\n */\nexport function isSearchFieldBlocklisted(\n field: string,\n entityType: string | null | undefined,\n config: SearchConfig,\n): boolean {\n const lower = field.toLowerCase()\n if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return true\n if (!entityType) return false\n const scoped = config.entityBlocklistedFields?.[entityType.trim().toLowerCase()]\n if (!Array.isArray(scoped) || !scoped.length) return false\n return scoped.some((blocked) => lower.includes(blocked))\n}\n\n/**\n * Browser-safe accessor for the minimum search token length.\n *\n * Why: client components (e.g. global search dialog) must mirror the server-side\n * tokenizer's `minTokenLength` so the UI gates the request before hitting an\n * empty result set. Pulling the value through this single helper keeps the env\n * contract (`OM_SEARCH_MIN_LEN`) authoritative on both sides.\n *\n * How to apply: call from anywhere \u2014 server, client (when the host app exposes\n * `OM_SEARCH_MIN_LEN` through `next.config.ts`'s `env` block), or tests.\n */\nexport function resolveSearchMinTokenLength(): number {\n return parseNumber(process.env.OM_SEARCH_MIN_LEN, DEFAULT_SEARCH_MIN_TOKEN_LENGTH, 1)\n}\n"],
5
- "mappings": "AAAA,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,+BAA+B;AAejC,MAAM,kCAAkC;AACxC,MAAM,iCAAiC;AACvC,MAAM,sCAAsC;AAC5C,MAAM,uCAAuC;AAQpD,MAAM,oBAAoB,CAAC,YAAY,SAAS,UAAU,MAAM;AAEhE,MAAM,6BAA6B;AAEnC,SAAS,aAAa,KAAyB,UAA4B;AACzE,SAAO,wBAAwB,KAAK,QAAQ;AAC9C;AAEA,SAAS,YAAY,KAAyB,UAAkB,MAAM,GAAW;AAC/E,SAAO,uBAAuB,KAAK,UAAU,EAAE,SAAS,MAAM,IAAI,CAAC;AACrE;AAEO,SAAS,yBAAyB,QAAyC;AAChF,QAAM,eAAe,CAAC,OAA2B,aAA6B;AAC5E,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,EAAG,QAAO;AACjD,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB;AACA,SAAO;AAAA,IACL,eAAe,aAAa,OAAO,eAAe,8BAA8B;AAAA,IAChF,mBAAmB,aAAa,OAAO,mBAAmB,mCAAmC;AAAA,IAC7F,oBAAoB,aAAa,OAAO,oBAAoB,oCAAoC;AAAA,EAClG;AACF;AAEA,SAAS,mBAAmB,KAAoD;AAC9E,QAAM,SAAS,OAAO,IAAI,KAAK,EAAE,YAAY;AAC7C,MAAI,UAAU,OAAQ,QAAO;AAC7B,MAAI,UAAU,MAAO,QAAO;AAC5B,SAAO;AACT;AAcA,SAAS,oBAAoB,KAG3B;AACA,QAAM,SAAmB,CAAC;AAC1B,QAAM,WAAW,oBAAI,IAAsB;AAE3C,aAAW,YAAY,wBAAwB,GAAG,GAAG;AACnD,UAAM,QAAQ,SAAS,YAAY;AACnC,UAAM,iBAAiB,MAAM,QAAQ,0BAA0B;AAC/D,UAAM,aAAa,kBAAkB,IAAI,MAAM,MAAM,GAAG,cAAc,EAAE,KAAK,IAAI;AACjF,UAAM,QAAQ,kBAAkB,IAAI,MAAM,MAAM,iBAAiB,CAAC,EAAE,KAAK,IAAI;AAC7E,QAAI,CAAC,MAAM,OAAQ;AAEnB,QAAI,CAAC,WAAW,QAAQ;AACtB,UAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO,KAAK,KAAK;AAC9C;AAAA,IACF;AAEA,UAAM,SAAS,SAAS,IAAI,UAAU,KAAK,CAAC;AAC5C,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO,KAAK,KAAK;AAC9C,aAAS,IAAI,YAAY,MAAM;AAAA,EACjC;AAEA,aAAW,YAAY,mBAAmB;AACxC,QAAI,CAAC,OAAO,SAAS,QAAQ,EAAG,QAAO,KAAK,QAAQ;AAAA,EACtD;AAEA,QAAM,kBAAkB,uBAAO,OAAO,IAAI;AAC1C,aAAW,CAAC,YAAY,MAAM,KAAK,SAAU,iBAAgB,UAAU,IAAI;AAE3E,SAAO,EAAE,QAAQ,UAAU,gBAAgB;AAC7C;AAEO,SAAS,sBAAoC;AAClD,QAAM,YAAY,oBAAoB,QAAQ,IAAI,yBAAyB;AAC3E,SAAO;AAAA,IACL,SAAS,aAAa,QAAQ,IAAI,mBAAmB,IAAI;AAAA,IACzD,gBAAgB,4BAA4B;AAAA,IAC5C,gBAAgB,aAAa,QAAQ,IAAI,0BAA0B,IAAI;AAAA,IACvE,eAAe,mBAAmB,QAAQ,IAAI,mBAAmB;AAAA,IACjE,gBAAgB,aAAa,QAAQ,IAAI,4BAA4B,KAAK;AAAA,IAC1E,mBAAmB,UAAU;AAAA,IAC7B,yBAAyB,UAAU;AAAA,IACnC,eAAe,YAAY,QAAQ,IAAI,2BAA2B,gCAAgC,CAAC;AAAA,IACnG,mBAAmB,YAAY,QAAQ,IAAI,gCAAgC,qCAAqC,CAAC;AAAA,IACjH,oBAAoB,YAAY,QAAQ,IAAI,iCAAiC,sCAAsC,CAAC;AAAA,EACtH;AACF;AAcO,SAAS,yBACd,OACA,YACA,QACS;AACT,QAAM,QAAQ,MAAM,YAAY;AAChC,MAAI,OAAO,kBAAkB,KAAK,CAAC,YAAY,MAAM,SAAS,OAAO,CAAC,EAAG,QAAO;AAChF,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,SAAS,OAAO,0BAA0B,WAAW,KAAK,EAAE,YAAY,CAAC;AAC/E,MAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,CAAC,OAAO,OAAQ,QAAO;AACrD,SAAO,OAAO,KAAK,CAAC,YAAY,MAAM,SAAS,OAAO,CAAC;AACzD;AAaO,SAAS,8BAAsC;AACpD,SAAO,YAAY,QAAQ,IAAI,mBAAmB,iCAAiC,CAAC;AACtF;",
4
+ "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseNumberWithDefault } from '@open-mercato/shared/lib/number'\n\nexport type SearchConfig = {\n enabled: boolean\n minTokenLength: number\n enablePartials: boolean\n hashAlgorithm: 'sha256' | 'sha1' | 'md5'\n storeRawTokens: boolean\n blocklistedFields: string[]\n}\n\nexport const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3\n\nconst DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']\n\nfunction parseBoolean(raw: string | undefined, fallback: boolean): boolean {\n return parseBooleanWithDefault(raw, fallback)\n}\n\nfunction parseNumber(raw: string | undefined, fallback: number, min = 1): number {\n return parseNumberWithDefault(raw, fallback, { integer: true, min })\n}\n\nfunction parseHashAlgorithm(raw: string | undefined): 'sha256' | 'sha1' | 'md5' {\n const value = (raw ?? '').trim().toLowerCase()\n if (value === 'sha1') return 'sha1'\n if (value === 'md5') return 'md5'\n return 'sha256'\n}\n\nexport function resolveSearchConfig(): SearchConfig {\n return {\n enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),\n minTokenLength: resolveSearchMinTokenLength(),\n enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),\n hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),\n storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),\n blocklistedFields: (process.env.OM_SEARCH_FIELD_BLOCKLIST ?? '')\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n .filter((value, index, arr) => arr.indexOf(value) === index)\n .map((entry) => entry.toLowerCase())\n .concat(DEFAULT_BLOCKLIST)\n .filter((value, index, arr) => arr.indexOf(value) === index),\n }\n}\n\n/**\n * Browser-safe accessor for the minimum search token length.\n *\n * Why: client components (e.g. global search dialog) must mirror the server-side\n * tokenizer's `minTokenLength` so the UI gates the request before hitting an\n * empty result set. Pulling the value through this single helper keeps the env\n * contract (`OM_SEARCH_MIN_LEN`) authoritative on both sides.\n *\n * How to apply: call from anywhere \u2014 server, client (when the host app exposes\n * `OM_SEARCH_MIN_LEN` through `next.config.ts`'s `env` block), or tests.\n */\nexport function resolveSearchMinTokenLength(): number {\n return parseNumber(process.env.OM_SEARCH_MIN_LEN, DEFAULT_SEARCH_MIN_TOKEN_LENGTH, 1)\n}\n"],
5
+ "mappings": "AAAA,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AAWhC,MAAM,kCAAkC;AAE/C,MAAM,oBAAoB,CAAC,YAAY,SAAS,UAAU,MAAM;AAEhE,SAAS,aAAa,KAAyB,UAA4B;AACzE,SAAO,wBAAwB,KAAK,QAAQ;AAC9C;AAEA,SAAS,YAAY,KAAyB,UAAkB,MAAM,GAAW;AAC/E,SAAO,uBAAuB,KAAK,UAAU,EAAE,SAAS,MAAM,IAAI,CAAC;AACrE;AAEA,SAAS,mBAAmB,KAAoD;AAC9E,QAAM,SAAS,OAAO,IAAI,KAAK,EAAE,YAAY;AAC7C,MAAI,UAAU,OAAQ,QAAO;AAC7B,MAAI,UAAU,MAAO,QAAO;AAC5B,SAAO;AACT;AAEO,SAAS,sBAAoC;AAClD,SAAO;AAAA,IACL,SAAS,aAAa,QAAQ,IAAI,mBAAmB,IAAI;AAAA,IACzD,gBAAgB,4BAA4B;AAAA,IAC5C,gBAAgB,aAAa,QAAQ,IAAI,0BAA0B,IAAI;AAAA,IACvE,eAAe,mBAAmB,QAAQ,IAAI,mBAAmB;AAAA,IACjE,gBAAgB,aAAa,QAAQ,IAAI,4BAA4B,KAAK;AAAA,IAC1E,oBAAoB,QAAQ,IAAI,6BAA6B,IAC1D,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,OAAO,CAAC,UAAU,MAAM,SAAS,CAAC,EAClC,OAAO,CAAC,OAAO,OAAO,QAAQ,IAAI,QAAQ,KAAK,MAAM,KAAK,EAC1D,IAAI,CAAC,UAAU,MAAM,YAAY,CAAC,EAClC,OAAO,iBAAiB,EACxB,OAAO,CAAC,OAAO,OAAO,QAAQ,IAAI,QAAQ,KAAK,MAAM,KAAK;AAAA,EAC/D;AACF;AAaO,SAAS,8BAAsC;AACpD,SAAO,YAAY,QAAQ,IAAI,mBAAmB,iCAAiC,CAAC;AACtF;",
6
6
  "names": []
7
7
  }
@@ -1,25 +1,18 @@
1
1
  import crypto from "crypto";
2
- import { resolveSearchConfig, resolveSearchTokenLimits } from "./config.js";
2
+ import { resolveSearchConfig } from "./config.js";
3
3
  function normalizeText(text) {
4
4
  return text.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[%_]/g, " ").toLowerCase();
5
5
  }
6
6
  function splitTokens(text, minLength) {
7
7
  return normalizeText(text).split(/[^a-z0-9]+/i).filter((token) => token.length >= minLength);
8
8
  }
9
- function appendExpandedToken(token, config, seen, tokens, limit) {
10
- const append = (candidate) => {
11
- if (seen.has(candidate)) return tokens.length < limit;
12
- seen.add(candidate);
13
- tokens.push(candidate);
14
- return tokens.length < limit;
15
- };
16
- if (!config.enablePartials) {
17
- append(token);
18
- return;
19
- }
20
- for (let length = config.minTokenLength; length <= token.length; length += 1) {
21
- if (!append(token.slice(0, length))) return;
9
+ function expandToken(token, config) {
10
+ if (!config.enablePartials) return [token];
11
+ const results = [];
12
+ for (let i = config.minTokenLength; i <= token.length; i += 1) {
13
+ results.push(token.slice(0, i));
22
14
  }
15
+ return results;
23
16
  }
24
17
  function hashToken(token, config) {
25
18
  const cfg = config ?? resolveSearchConfig();
@@ -27,15 +20,10 @@ function hashToken(token, config) {
27
20
  }
28
21
  function tokenizeText(text, config) {
29
22
  const cfg = config ?? resolveSearchConfig();
30
- const limits = resolveSearchTokenLimits(cfg);
31
- const boundedText = limits.maxFieldChars > 0 ? text.slice(0, limits.maxFieldChars) : text;
32
- const tokenLimit = limits.maxTokensPerField > 0 ? limits.maxTokensPerField : Number.POSITIVE_INFINITY;
33
- const seen = /* @__PURE__ */ new Set();
34
- const tokens = [];
35
- for (const token of splitTokens(boundedText, cfg.minTokenLength)) {
36
- if (tokens.length >= tokenLimit) break;
37
- appendExpandedToken(token, cfg, seen, tokens, tokenLimit);
38
- }
23
+ const baseTokens = splitTokens(text, cfg.minTokenLength);
24
+ const expanded = baseTokens.flatMap((token) => expandToken(token, cfg));
25
+ const unique = Array.from(new Set(expanded));
26
+ const tokens = unique.filter((token) => token.length >= cfg.minTokenLength);
39
27
  const hashes = tokens.map((token) => hashToken(token, cfg));
40
28
  return { tokens, hashes };
41
29
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/search/tokenize.ts"],
4
- "sourcesContent": ["import crypto from 'crypto'\nimport { resolveSearchConfig, resolveSearchTokenLimits, type SearchConfig } from './config'\n\nexport type TokenizationResult = {\n tokens: string[]\n hashes: string[]\n}\n\nfunction normalizeText(text: string): string {\n return text\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[%_]/g, ' ')\n .toLowerCase()\n}\n\nfunction splitTokens(text: string, minLength: number): string[] {\n return normalizeText(text)\n .split(/[^a-z0-9]+/i)\n .filter((token) => token.length >= minLength)\n}\n\nfunction appendExpandedToken(\n token: string,\n config: SearchConfig,\n seen: Set<string>,\n tokens: string[],\n limit: number,\n): void {\n const append = (candidate: string): boolean => {\n if (seen.has(candidate)) return tokens.length < limit\n seen.add(candidate)\n tokens.push(candidate)\n return tokens.length < limit\n }\n\n if (!config.enablePartials) {\n append(token)\n return\n }\n\n for (let length = config.minTokenLength; length <= token.length; length += 1) {\n if (!append(token.slice(0, length))) return\n }\n}\n\nexport function hashToken(token: string, config?: SearchConfig): string {\n const cfg = config ?? resolveSearchConfig()\n return crypto.createHash(cfg.hashAlgorithm).update(token).digest('hex')\n}\n\nexport function tokenizeText(text: string, config?: SearchConfig): TokenizationResult {\n const cfg = config ?? resolveSearchConfig()\n const limits = resolveSearchTokenLimits(cfg)\n const boundedText = limits.maxFieldChars > 0 ? text.slice(0, limits.maxFieldChars) : text\n const tokenLimit = limits.maxTokensPerField > 0 ? limits.maxTokensPerField : Number.POSITIVE_INFINITY\n const seen = new Set<string>()\n const tokens: string[] = []\n\n for (const token of splitTokens(boundedText, cfg.minTokenLength)) {\n if (tokens.length >= tokenLimit) break\n appendExpandedToken(token, cfg, seen, tokens, tokenLimit)\n }\n\n const hashes = tokens.map((token) => hashToken(token, cfg))\n return { tokens, hashes }\n}\n"],
5
- "mappings": "AAAA,OAAO,YAAY;AACnB,SAAS,qBAAqB,gCAAmD;AAOjF,SAAS,cAAc,MAAsB;AAC3C,SAAO,KACJ,UAAU,MAAM,EAChB,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,SAAS,GAAG,EACpB,YAAY;AACjB;AAEA,SAAS,YAAY,MAAc,WAA6B;AAC9D,SAAO,cAAc,IAAI,EACtB,MAAM,aAAa,EACnB,OAAO,CAAC,UAAU,MAAM,UAAU,SAAS;AAChD;AAEA,SAAS,oBACP,OACA,QACA,MACA,QACA,OACM;AACN,QAAM,SAAS,CAAC,cAA+B;AAC7C,QAAI,KAAK,IAAI,SAAS,EAAG,QAAO,OAAO,SAAS;AAChD,SAAK,IAAI,SAAS;AAClB,WAAO,KAAK,SAAS;AACrB,WAAO,OAAO,SAAS;AAAA,EACzB;AAEA,MAAI,CAAC,OAAO,gBAAgB;AAC1B,WAAO,KAAK;AACZ;AAAA,EACF;AAEA,WAAS,SAAS,OAAO,gBAAgB,UAAU,MAAM,QAAQ,UAAU,GAAG;AAC5E,QAAI,CAAC,OAAO,MAAM,MAAM,GAAG,MAAM,CAAC,EAAG;AAAA,EACvC;AACF;AAEO,SAAS,UAAU,OAAe,QAA+B;AACtE,QAAM,MAAM,UAAU,oBAAoB;AAC1C,SAAO,OAAO,WAAW,IAAI,aAAa,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxE;AAEO,SAAS,aAAa,MAAc,QAA2C;AACpF,QAAM,MAAM,UAAU,oBAAoB;AAC1C,QAAM,SAAS,yBAAyB,GAAG;AAC3C,QAAM,cAAc,OAAO,gBAAgB,IAAI,KAAK,MAAM,GAAG,OAAO,aAAa,IAAI;AACrF,QAAM,aAAa,OAAO,oBAAoB,IAAI,OAAO,oBAAoB,OAAO;AACpF,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,SAAmB,CAAC;AAE1B,aAAW,SAAS,YAAY,aAAa,IAAI,cAAc,GAAG;AAChE,QAAI,OAAO,UAAU,WAAY;AACjC,wBAAoB,OAAO,KAAK,MAAM,QAAQ,UAAU;AAAA,EAC1D;AAEA,QAAM,SAAS,OAAO,IAAI,CAAC,UAAU,UAAU,OAAO,GAAG,CAAC;AAC1D,SAAO,EAAE,QAAQ,OAAO;AAC1B;",
4
+ "sourcesContent": ["import crypto from 'crypto'\nimport { resolveSearchConfig, type SearchConfig } from './config'\n\nexport type TokenizationResult = {\n tokens: string[]\n hashes: string[]\n}\n\nfunction normalizeText(text: string): string {\n return text\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[%_]/g, ' ')\n .toLowerCase()\n}\n\nfunction splitTokens(text: string, minLength: number): string[] {\n return normalizeText(text)\n .split(/[^a-z0-9]+/i)\n .filter((token) => token.length >= minLength)\n}\n\nfunction expandToken(token: string, config: SearchConfig): string[] {\n if (!config.enablePartials) return [token]\n const results: string[] = []\n for (let i = config.minTokenLength; i <= token.length; i += 1) {\n results.push(token.slice(0, i))\n }\n return results\n}\n\nexport function hashToken(token: string, config?: SearchConfig): string {\n const cfg = config ?? resolveSearchConfig()\n return crypto.createHash(cfg.hashAlgorithm).update(token).digest('hex')\n}\n\nexport function tokenizeText(text: string, config?: SearchConfig): TokenizationResult {\n const cfg = config ?? resolveSearchConfig()\n const baseTokens = splitTokens(text, cfg.minTokenLength)\n const expanded = baseTokens.flatMap((token) => expandToken(token, cfg))\n const unique = Array.from(new Set(expanded))\n const tokens = unique.filter((token) => token.length >= cfg.minTokenLength)\n const hashes = tokens.map((token) => hashToken(token, cfg))\n return { tokens, hashes }\n}\n"],
5
+ "mappings": "AAAA,OAAO,YAAY;AACnB,SAAS,2BAA8C;AAOvD,SAAS,cAAc,MAAsB;AAC3C,SAAO,KACJ,UAAU,MAAM,EAChB,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,SAAS,GAAG,EACpB,YAAY;AACjB;AAEA,SAAS,YAAY,MAAc,WAA6B;AAC9D,SAAO,cAAc,IAAI,EACtB,MAAM,aAAa,EACnB,OAAO,CAAC,UAAU,MAAM,UAAU,SAAS;AAChD;AAEA,SAAS,YAAY,OAAe,QAAgC;AAClE,MAAI,CAAC,OAAO,eAAgB,QAAO,CAAC,KAAK;AACzC,QAAM,UAAoB,CAAC;AAC3B,WAAS,IAAI,OAAO,gBAAgB,KAAK,MAAM,QAAQ,KAAK,GAAG;AAC7D,YAAQ,KAAK,MAAM,MAAM,GAAG,CAAC,CAAC;AAAA,EAChC;AACA,SAAO;AACT;AAEO,SAAS,UAAU,OAAe,QAA+B;AACtE,QAAM,MAAM,UAAU,oBAAoB;AAC1C,SAAO,OAAO,WAAW,IAAI,aAAa,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxE;AAEO,SAAS,aAAa,MAAc,QAA2C;AACpF,QAAM,MAAM,UAAU,oBAAoB;AAC1C,QAAM,aAAa,YAAY,MAAM,IAAI,cAAc;AACvD,QAAM,WAAW,WAAW,QAAQ,CAAC,UAAU,YAAY,OAAO,GAAG,CAAC;AACtE,QAAM,SAAS,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC;AAC3C,QAAM,SAAS,OAAO,OAAO,CAAC,UAAU,MAAM,UAAU,IAAI,cAAc;AAC1E,QAAM,SAAS,OAAO,IAAI,CAAC,UAAU,UAAU,OAAO,GAAG,CAAC;AAC1D,SAAO,EAAE,QAAQ,OAAO;AAC1B;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.7-develop.6860.1.f796b22e80";
1
+ const APP_VERSION = "0.6.7";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6860.1.f796b22e80';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -1,24 +1,12 @@
1
1
  import { signWebhookPayload, buildWebhookHeaders, generateMessageId } from "./sign.js";
2
- import { WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, isWebhookTimestampWithinTolerance, verifyWebhookSignature } from "./verify.js";
2
+ import { verifyWebhookSignature } from "./verify.js";
3
3
  import { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from "./secrets.js";
4
- import {
5
- DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
6
- readBoundedRequestBody,
7
- resolveWebhookBodyLimitBytes,
8
- WebhookBodyTooLargeError
9
- } from "./body.js";
10
4
  export {
11
- DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
12
- WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
13
- WebhookBodyTooLargeError,
14
5
  buildWebhookHeaders,
15
6
  generateMessageId,
16
7
  generateWebhookSecret,
17
8
  isValidWebhookSecret,
18
- isWebhookTimestampWithinTolerance,
19
9
  parseWebhookSecret,
20
- readBoundedRequestBody,
21
- resolveWebhookBodyLimitBytes,
22
10
  signWebhookPayload,
23
11
  verifyWebhookSignature
24
12
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/webhooks/index.ts"],
4
- "sourcesContent": ["export { signWebhookPayload, buildWebhookHeaders, generateMessageId } from './sign'\nexport { WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, isWebhookTimestampWithinTolerance, verifyWebhookSignature } from './verify'\nexport { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from './secrets'\nexport {\n DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,\n readBoundedRequestBody,\n resolveWebhookBodyLimitBytes,\n WebhookBodyTooLargeError,\n} from './body'\nexport type { StandardWebhookHeaders, WebhookSigningKey, WebhookVerificationResult, StandardWebhookPayload } from './types'\nexport type {\n InboundWebhookRequest,\n WebhookSourceCredentialField,\n WebhookSourceConfig,\n WebhookHandlerMeta,\n WebhookHandlerPayload,\n WebhookHandlerContext,\n WebhookHandler,\n WebhookHandlerRegistryEntry,\n WebhookHandlerResult,\n WebhookIngestionStatus,\n} from './inbound-types'\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB,qBAAqB,yBAAyB;AAC3E,SAAS,qCAAqC,mCAAmC,8BAA8B;AAC/G,SAAS,uBAAuB,oBAAoB,4BAA4B;AAChF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
4
+ "sourcesContent": ["export { signWebhookPayload, buildWebhookHeaders, generateMessageId } from './sign'\nexport { verifyWebhookSignature } from './verify'\nexport { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from './secrets'\nexport type { StandardWebhookHeaders, WebhookSigningKey, WebhookVerificationResult, StandardWebhookPayload } from './types'\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB,qBAAqB,yBAAyB;AAC3E,SAAS,8BAA8B;AACvC,SAAS,uBAAuB,oBAAoB,4BAA4B;",
6
6
  "names": []
7
7
  }
@@ -1,16 +1,13 @@
1
1
  import { timingSafeEqual, createHmac } from "node:crypto";
2
2
  import { parseWebhookSecret } from "./secrets.js";
3
- const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
4
- function isWebhookTimestampWithinTolerance(timestamp, toleranceSeconds = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, nowSeconds = Math.floor(Date.now() / 1e3)) {
5
- const normalizedTimestamp = timestamp.trim();
6
- if (!/^\d+$/.test(normalizedTimestamp)) {
7
- return false;
3
+ const TOLERANCE_SECONDS = 5 * 60;
4
+ function verifyWebhookSignature(msgId, timestamp, body, signatureHeader, secrets, toleranceSeconds = TOLERANCE_SECONDS) {
5
+ const timestampNum = parseInt(timestamp, 10);
6
+ if (isNaN(timestampNum)) {
7
+ return { valid: false };
8
8
  }
9
- const timestampNum = Number.parseInt(normalizedTimestamp, 10);
10
- return Math.abs(nowSeconds - timestampNum) <= toleranceSeconds;
11
- }
12
- function verifyWebhookSignature(msgId, timestamp, body, signatureHeader, secrets, toleranceSeconds = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS) {
13
- if (!isWebhookTimestampWithinTolerance(timestamp, toleranceSeconds)) {
9
+ const now = Math.floor(Date.now() / 1e3);
10
+ if (Math.abs(now - timestampNum) > toleranceSeconds) {
14
11
  return { valid: false };
15
12
  }
16
13
  const signedContent = `${msgId}.${timestamp}.${body}`;
@@ -32,8 +29,6 @@ function verifyWebhookSignature(msgId, timestamp, body, signatureHeader, secrets
32
29
  return { valid: false };
33
30
  }
34
31
  export {
35
- WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
36
- isWebhookTimestampWithinTolerance,
37
32
  verifyWebhookSignature
38
33
  };
39
34
  //# sourceMappingURL=verify.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/webhooks/verify.ts"],
4
- "sourcesContent": ["import { timingSafeEqual, createHmac } from 'node:crypto'\nimport { parseWebhookSecret } from './secrets'\nimport type { WebhookVerificationResult } from './types'\n\nexport const WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 5 * 60 // 5 minutes\n\nexport function isWebhookTimestampWithinTolerance(\n timestamp: string,\n toleranceSeconds: number = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,\n nowSeconds: number = Math.floor(Date.now() / 1000),\n): boolean {\n const normalizedTimestamp = timestamp.trim()\n if (!/^\\d+$/.test(normalizedTimestamp)) {\n return false\n }\n\n const timestampNum = Number.parseInt(normalizedTimestamp, 10)\n return Math.abs(nowSeconds - timestampNum) <= toleranceSeconds\n}\n\n/**\n * Verify a Standard Webhooks signature against one or more secrets.\n * Returns verification result with which key matched.\n */\nexport function verifyWebhookSignature(\n msgId: string,\n timestamp: string,\n body: string,\n signatureHeader: string,\n secrets: string[],\n toleranceSeconds: number = WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,\n): WebhookVerificationResult {\n if (!isWebhookTimestampWithinTolerance(timestamp, toleranceSeconds)) {\n return { valid: false }\n }\n\n const signedContent = `${msgId}.${timestamp}.${body}`\n const providedSignatures = signatureHeader.split(' ')\n\n for (let keyIndex = 0; keyIndex < secrets.length; keyIndex++) {\n const key = parseWebhookSecret(secrets[keyIndex])\n const expectedSig = createHmac('sha256', key)\n .update(signedContent)\n .digest('base64')\n\n for (const sig of providedSignatures) {\n const parts = sig.split(',')\n if (parts.length !== 2 || parts[0] !== 'v1') continue\n\n const sigBytes = Buffer.from(parts[1], 'base64')\n const expectedBytes = Buffer.from(expectedSig, 'base64')\n\n if (sigBytes.length !== expectedBytes.length) continue\n\n if (timingSafeEqual(sigBytes, expectedBytes)) {\n return { valid: true, matchedKeyIndex: keyIndex }\n }\n }\n }\n\n return { valid: false }\n}\n"],
5
- "mappings": "AAAA,SAAS,iBAAiB,kBAAkB;AAC5C,SAAS,0BAA0B;AAG5B,MAAM,sCAAsC,IAAI;AAEhD,SAAS,kCACd,WACA,mBAA2B,qCAC3B,aAAqB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GACxC;AACT,QAAM,sBAAsB,UAAU,KAAK;AAC3C,MAAI,CAAC,QAAQ,KAAK,mBAAmB,GAAG;AACtC,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,OAAO,SAAS,qBAAqB,EAAE;AAC5D,SAAO,KAAK,IAAI,aAAa,YAAY,KAAK;AAChD;AAMO,SAAS,uBACd,OACA,WACA,MACA,iBACA,SACA,mBAA2B,qCACA;AAC3B,MAAI,CAAC,kCAAkC,WAAW,gBAAgB,GAAG;AACnE,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAEA,QAAM,gBAAgB,GAAG,KAAK,IAAI,SAAS,IAAI,IAAI;AACnD,QAAM,qBAAqB,gBAAgB,MAAM,GAAG;AAEpD,WAAS,WAAW,GAAG,WAAW,QAAQ,QAAQ,YAAY;AAC5D,UAAM,MAAM,mBAAmB,QAAQ,QAAQ,CAAC;AAChD,UAAM,cAAc,WAAW,UAAU,GAAG,EACzC,OAAO,aAAa,EACpB,OAAO,QAAQ;AAElB,eAAW,OAAO,oBAAoB;AACpC,YAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,UAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,KAAM;AAE7C,YAAM,WAAW,OAAO,KAAK,MAAM,CAAC,GAAG,QAAQ;AAC/C,YAAM,gBAAgB,OAAO,KAAK,aAAa,QAAQ;AAEvD,UAAI,SAAS,WAAW,cAAc,OAAQ;AAE9C,UAAI,gBAAgB,UAAU,aAAa,GAAG;AAC5C,eAAO,EAAE,OAAO,MAAM,iBAAiB,SAAS;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,MAAM;AACxB;",
4
+ "sourcesContent": ["import { timingSafeEqual, createHmac } from 'node:crypto'\nimport { parseWebhookSecret } from './secrets'\nimport type { WebhookVerificationResult } from './types'\n\nconst TOLERANCE_SECONDS = 5 * 60 // 5 minutes\n\n/**\n * Verify a Standard Webhooks signature against one or more secrets.\n * Returns verification result with which key matched.\n */\nexport function verifyWebhookSignature(\n msgId: string,\n timestamp: string,\n body: string,\n signatureHeader: string,\n secrets: string[],\n toleranceSeconds: number = TOLERANCE_SECONDS,\n): WebhookVerificationResult {\n const timestampNum = parseInt(timestamp, 10)\n if (isNaN(timestampNum)) {\n return { valid: false }\n }\n\n const now = Math.floor(Date.now() / 1000)\n if (Math.abs(now - timestampNum) > toleranceSeconds) {\n return { valid: false }\n }\n\n const signedContent = `${msgId}.${timestamp}.${body}`\n const providedSignatures = signatureHeader.split(' ')\n\n for (let keyIndex = 0; keyIndex < secrets.length; keyIndex++) {\n const key = parseWebhookSecret(secrets[keyIndex])\n const expectedSig = createHmac('sha256', key)\n .update(signedContent)\n .digest('base64')\n\n for (const sig of providedSignatures) {\n const parts = sig.split(',')\n if (parts.length !== 2 || parts[0] !== 'v1') continue\n\n const sigBytes = Buffer.from(parts[1], 'base64')\n const expectedBytes = Buffer.from(expectedSig, 'base64')\n\n if (sigBytes.length !== expectedBytes.length) continue\n\n if (timingSafeEqual(sigBytes, expectedBytes)) {\n return { valid: true, matchedKeyIndex: keyIndex }\n }\n }\n }\n\n return { valid: false }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB,kBAAkB;AAC5C,SAAS,0BAA0B;AAGnC,MAAM,oBAAoB,IAAI;AAMvB,SAAS,uBACd,OACA,WACA,MACA,iBACA,SACA,mBAA2B,mBACA;AAC3B,QAAM,eAAe,SAAS,WAAW,EAAE;AAC3C,MAAI,MAAM,YAAY,GAAG;AACvB,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAEA,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,MAAI,KAAK,IAAI,MAAM,YAAY,IAAI,kBAAkB;AACnD,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAEA,QAAM,gBAAgB,GAAG,KAAK,IAAI,SAAS,IAAI,IAAI;AACnD,QAAM,qBAAqB,gBAAgB,MAAM,GAAG;AAEpD,WAAS,WAAW,GAAG,WAAW,QAAQ,QAAQ,YAAY;AAC5D,UAAM,MAAM,mBAAmB,QAAQ,QAAQ,CAAC;AAChD,UAAM,cAAc,WAAW,UAAU,GAAG,EACzC,OAAO,aAAa,EACpB,OAAO,QAAQ;AAElB,eAAW,OAAO,oBAAoB;AACpC,YAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,UAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,KAAM;AAE7C,YAAM,WAAW,OAAO,KAAK,MAAM,CAAC,GAAG,QAAQ;AAC/C,YAAM,gBAAgB,OAAO,KAAK,aAAa,QAAQ;AAEvD,UAAI,SAAS,WAAW,cAAc,OAAQ;AAE9C,UAAI,gBAAgB,UAAU,aAAa,GAAG;AAC5C,eAAO,EAAE,OAAO,MAAM,iBAAiB,SAAS;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,MAAM;AACxB;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/modules/analytics.ts"],
4
- "sourcesContent": ["import { createLogger } from '../lib/logger'\n\nconst logger = createLogger('shared').child({ component: 'analytics-registry' })\n\nexport type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'\n\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nexport type AnalyticsFieldType = 'numeric' | 'text' | 'uuid' | 'timestamp' | 'jsonb'\n\nexport type AnalyticsEntityTypeConfig = {\n tableName: string\n schema?: string\n dateField: string\n defaultScopeFields: string[]\n /**\n * Field name (a key of `fieldMappings`) holding the per-row currency of monetary\n * columns. Declaring it lets analytics consumers verify that the rows they aggregate\n * all carry one currency before labelling the result with it (#4676). Omit it for\n * entities whose amounts are not per-row denominated.\n */\n currencyField?: string\n}\n\nexport type AnalyticsFieldMapping = {\n dbColumn: string\n type: AnalyticsFieldType\n}\n\nexport type AnalyticsLabelResolverConfig = {\n table: string\n idColumn: string\n labelColumn: string\n}\n\nexport type AnalyticsEntityConfig = {\n entityId: string\n requiredFeatures: string[]\n entityConfig: AnalyticsEntityTypeConfig\n fieldMappings: Record<string, AnalyticsFieldMapping>\n labelResolvers?: Record<string, AnalyticsLabelResolverConfig>\n}\n\nexport type AnalyticsModuleConfig = {\n entities: AnalyticsEntityConfig[]\n}\n\nlet _analyticsModuleConfigs: AnalyticsModuleConfig[] | null = null\n\nexport function registerAnalyticsModuleConfigs(configs: AnalyticsModuleConfig[]): void {\n if (_analyticsModuleConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Analytics module configs re-registered (this may occur during HMR)')\n }\n _analyticsModuleConfigs = configs\n}\n\nexport function getAnalyticsModuleConfigs(): AnalyticsModuleConfig[] {\n return _analyticsModuleConfigs ?? []\n}\n\nexport function getAllAnalyticsEntityConfigs(): AnalyticsEntityConfig[] {\n const configs = getAnalyticsModuleConfigs()\n return configs.flatMap((moduleConfig) => moduleConfig.entities)\n}\n\nexport function getAnalyticsEntityConfig(entityId: string): AnalyticsEntityConfig | null {\n const entities = getAllAnalyticsEntityConfigs()\n return entities.find((e) => e.entityId === entityId) ?? null\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AA6C/E,IAAI,0BAA0D;AAEvD,SAAS,+BAA+B,SAAwC;AACrF,MAAI,4BAA4B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,WAAO,MAAM,oEAAoE;AAAA,EACnF;AACA,4BAA0B;AAC5B;AAEO,SAAS,4BAAqD;AACnE,SAAO,2BAA2B,CAAC;AACrC;AAEO,SAAS,+BAAwD;AACtE,QAAM,UAAU,0BAA0B;AAC1C,SAAO,QAAQ,QAAQ,CAAC,iBAAiB,aAAa,QAAQ;AAChE;AAEO,SAAS,yBAAyB,UAAgD;AACvF,QAAM,WAAW,6BAA6B;AAC9C,SAAO,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,KAAK;AAC1D;",
4
+ "sourcesContent": ["import { createLogger } from '../lib/logger'\n\nconst logger = createLogger('shared').child({ component: 'analytics-registry' })\n\nexport type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'\n\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nexport type AnalyticsFieldType = 'numeric' | 'text' | 'uuid' | 'timestamp' | 'jsonb'\n\nexport type AnalyticsEntityTypeConfig = {\n tableName: string\n schema?: string\n dateField: string\n defaultScopeFields: string[]\n}\n\nexport type AnalyticsFieldMapping = {\n dbColumn: string\n type: AnalyticsFieldType\n}\n\nexport type AnalyticsLabelResolverConfig = {\n table: string\n idColumn: string\n labelColumn: string\n}\n\nexport type AnalyticsEntityConfig = {\n entityId: string\n requiredFeatures: string[]\n entityConfig: AnalyticsEntityTypeConfig\n fieldMappings: Record<string, AnalyticsFieldMapping>\n labelResolvers?: Record<string, AnalyticsLabelResolverConfig>\n}\n\nexport type AnalyticsModuleConfig = {\n entities: AnalyticsEntityConfig[]\n}\n\nlet _analyticsModuleConfigs: AnalyticsModuleConfig[] | null = null\n\nexport function registerAnalyticsModuleConfigs(configs: AnalyticsModuleConfig[]): void {\n if (_analyticsModuleConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Analytics module configs re-registered (this may occur during HMR)')\n }\n _analyticsModuleConfigs = configs\n}\n\nexport function getAnalyticsModuleConfigs(): AnalyticsModuleConfig[] {\n return _analyticsModuleConfigs ?? []\n}\n\nexport function getAllAnalyticsEntityConfigs(): AnalyticsEntityConfig[] {\n const configs = getAnalyticsModuleConfigs()\n return configs.flatMap((moduleConfig) => moduleConfig.entities)\n}\n\nexport function getAnalyticsEntityConfig(entityId: string): AnalyticsEntityConfig | null {\n const entities = getAllAnalyticsEntityConfigs()\n return entities.find((e) => e.entityId === entityId) ?? null\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AAsC/E,IAAI,0BAA0D;AAEvD,SAAS,+BAA+B,SAAwC;AACrF,MAAI,4BAA4B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,WAAO,MAAM,oEAAoE;AAAA,EACnF;AACA,4BAA0B;AAC5B;AAEO,SAAS,4BAAqD;AACnE,SAAO,2BAA2B,CAAC;AACrC;AAEO,SAAS,+BAAwD;AACtE,QAAM,UAAU,0BAA0B;AAC1C,SAAO,QAAQ,QAAQ,CAAC,iBAAiB,aAAa,QAAQ;AAChE;AAEO,SAAS,yBAAyB,UAAgD;AACvF,QAAM,WAAW,6BAA6B;AAC9C,SAAO,SAAS,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,KAAK;AAC1D;",
6
6
  "names": []
7
7
  }
@@ -23,8 +23,7 @@ const DOMAIN_KEYS = [
23
23
  "setup",
24
24
  "acl",
25
25
  "di",
26
- "encryption",
27
- "nav"
26
+ "encryption"
28
27
  ];
29
28
  const TRACKING_ISSUE_HINT = "See `.ai/specs/implemented/2026-05-04-modules-ts-unified-overrides.md` and tracking issue https://github.com/open-mercato/open-mercato/issues/1787.";
30
29
  function applyModuleOverridesFromEnabledModules(modules) {
@@ -75,33 +74,6 @@ const aclFeatureOverrideStore = { modules: {}, programmatic: {} };
75
74
  const encryptionMapOverrideStore = { modules: {}, programmatic: {} };
76
75
  const diOverrideStore = { modules: {}, programmatic: {} };
77
76
  const setupOverridesByModule = {};
78
- const GLOBAL_NAV_OVERRIDE_STATE_KEY = "__openMercatoNavOverrideState__";
79
- function getNavOverrideState() {
80
- const existing = globalThis[GLOBAL_NAV_OVERRIDE_STATE_KEY];
81
- if (existing && typeof existing === "object") {
82
- const typed = existing;
83
- if ("modules" in typed && "programmatic" in typed) return typed;
84
- }
85
- const initial = { modules: null, programmatic: null };
86
- globalThis[GLOBAL_NAV_OVERRIDE_STATE_KEY] = initial;
87
- return initial;
88
- }
89
- function normalizeNavGroupOrder(value) {
90
- if (!Array.isArray(value)) return null;
91
- const ids = Array.from(
92
- new Set(
93
- value.filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim())
94
- )
95
- );
96
- return ids.length > 0 ? ids : null;
97
- }
98
- function applyNavGroupOrderOverrides(groupOrder) {
99
- getNavOverrideState().programmatic = groupOrder === null ? null : normalizeNavGroupOrder(groupOrder);
100
- }
101
- function getNavGroupOrderOverride() {
102
- const state = getNavOverrideState();
103
- return state.programmatic ?? state.modules?.groupOrder ?? null;
104
- }
105
77
  function normalizeIdOverrideKey(key, label) {
106
78
  if (typeof key !== "string") return null;
107
79
  const trimmed = key.trim();
@@ -289,9 +261,6 @@ function resetModuleContractOverridesForTests() {
289
261
  clearStore(encryptionMapOverrideStore);
290
262
  clearStore(diOverrideStore);
291
263
  for (const key of Object.keys(setupOverridesByModule)) delete setupOverridesByModule[key];
292
- const navState = getNavOverrideState();
293
- navState.modules = null;
294
- navState.programmatic = null;
295
264
  }
296
265
  function composeApiRouteOverrides() {
297
266
  const modulesKeys = Object.keys(modulesConfigApiRouteOverrides);
@@ -858,23 +827,7 @@ function encryptionOverridesApplier(entries) {
858
827
  applyStoreOverrides(encryptionMapOverrideStore, "modules", entry.overrides?.maps, { label: "encryption.maps" });
859
828
  }
860
829
  }
861
- function navOverridesApplier(entries) {
862
- const state = getNavOverrideState();
863
- for (const entry of entries) {
864
- const groupOrder = normalizeNavGroupOrder(entry.overrides?.groupOrder);
865
- if (!groupOrder) continue;
866
- if (state.modules && state.modules.moduleId !== entry.moduleId) {
867
- logger.warn("nav.groupOrder declared by more than one module \u2014 the later one wins", {
868
- previousModuleId: state.modules.moduleId,
869
- moduleId: entry.moduleId,
870
- hint: "Sidebar group ordering is a single app-wide decision; declare it on one module entry."
871
- });
872
- }
873
- state.modules = { moduleId: entry.moduleId, groupOrder };
874
- }
875
- }
876
830
  function registerBuiltInModuleOverrideAppliers() {
877
- registerModuleOverrideApplier("nav", navOverridesApplier);
878
831
  registerModuleOverrideApplier("routes", routesOverridesApplier);
879
832
  registerModuleOverrideApplier("events", eventsOverridesApplier);
880
833
  registerModuleOverrideApplier("workers", workersOverridesApplier);
@@ -912,7 +865,6 @@ export {
912
865
  applyInjectionWidgetOverridesToTables,
913
866
  applyModuleOverridesFromEnabledModules,
914
867
  applyModuleOverridesToModules,
915
- applyNavGroupOrderOverrides,
916
868
  applyNotificationHandlerOverrides,
917
869
  applyNotificationHandlerOverridesToEntries,
918
870
  applyNotificationTypeOverrides,
@@ -943,7 +895,6 @@ export {
943
895
  composeResponseEnricherOverrides,
944
896
  composeSubscriberOverrides,
945
897
  composeWorkerOverrides,
946
- getNavGroupOrderOverride,
947
898
  registerModuleOverrideApplier,
948
899
  resetApiRouteOverridesForTests,
949
900
  resetModuleContractOverridesForTests,