@open-mercato/core 0.6.5-develop.5337.1.534b781eac → 0.6.5

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 (350) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/bootstrap.js +46 -6
  4. package/dist/bootstrap.js.map +2 -2
  5. package/dist/generated/entities/organization/index.js +2 -0
  6. package/dist/generated/entities/organization/index.js.map +2 -2
  7. package/dist/generated/entity-fields-registry.js +1 -0
  8. package/dist/generated/entity-fields-registry.js.map +2 -2
  9. package/dist/helpers/integration/crmFixtures.js +4 -0
  10. package/dist/helpers/integration/crmFixtures.js.map +2 -2
  11. package/dist/modules/attachments/api/library/route.js +2 -2
  12. package/dist/modules/attachments/api/library/route.js.map +2 -2
  13. package/dist/modules/attachments/api/route.js +2 -0
  14. package/dist/modules/attachments/api/route.js.map +2 -2
  15. package/dist/modules/attachments/components/AttachmentContentPreview.js +9 -5
  16. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  17. package/dist/modules/attachments/lib/access.js +18 -0
  18. package/dist/modules/attachments/lib/access.js.map +2 -2
  19. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +3 -2
  20. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
  21. package/dist/modules/audit_logs/data/entities.js +2 -1
  22. package/dist/modules/audit_logs/data/entities.js.map +2 -2
  23. package/dist/modules/audit_logs/migrations/Migration20260611104500.js +13 -0
  24. package/dist/modules/audit_logs/migrations/Migration20260611104500.js.map +7 -0
  25. package/dist/modules/audit_logs/services/accessLogService.js +10 -0
  26. package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
  27. package/dist/modules/auth/api/admin/nav.js +9 -0
  28. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  29. package/dist/modules/auth/api/login.js +4 -13
  30. package/dist/modules/auth/api/login.js.map +2 -2
  31. package/dist/modules/auth/commands/users.js +20 -14
  32. package/dist/modules/auth/commands/users.js.map +2 -2
  33. package/dist/modules/auth/data/entities.js +4 -2
  34. package/dist/modules/auth/data/entities.js.map +2 -2
  35. package/dist/modules/auth/lib/backendChrome.js +35 -2
  36. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  37. package/dist/modules/auth/lib/consentIntegrity.js +3 -3
  38. package/dist/modules/auth/lib/consentIntegrity.js.map +2 -2
  39. package/dist/modules/auth/migrations/Migration20260610120000.js +30 -0
  40. package/dist/modules/auth/migrations/Migration20260610120000.js.map +7 -0
  41. package/dist/modules/auth/migrations/Migration20260611103000.js +15 -0
  42. package/dist/modules/auth/migrations/Migration20260611103000.js.map +7 -0
  43. package/dist/modules/auth/services/authService.js +5 -3
  44. package/dist/modules/auth/services/authService.js.map +2 -2
  45. package/dist/modules/auth/services/rbacService.js +3 -2
  46. package/dist/modules/auth/services/rbacService.js.map +2 -2
  47. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +1 -1
  48. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +1 -1
  49. package/dist/modules/catalog/ai-tools/products-pack.js.map +1 -1
  50. package/dist/modules/catalog/ai-tools/variants-pack.js.map +1 -1
  51. package/dist/modules/communication_channels/data/entities.js.map +1 -1
  52. package/dist/modules/communication_channels/encryption.js.map +1 -1
  53. package/dist/modules/communication_channels/lib/thread-matcher.js.map +1 -1
  54. package/dist/modules/communication_channels/lib/thread-token.js.map +1 -1
  55. package/dist/modules/currencies/api/currencies/route.js +4 -3
  56. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  57. package/dist/modules/customer_accounts/api/admin/roles.js +2 -1
  58. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  59. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
  60. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
  61. package/dist/modules/customer_accounts/events.js +1 -1
  62. package/dist/modules/customer_accounts/events.js.map +1 -1
  63. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +1 -1
  64. package/dist/modules/customers/acl.js +1 -1
  65. package/dist/modules/customers/acl.js.map +1 -1
  66. package/dist/modules/customers/ai-tools/companies-pack.js.map +1 -1
  67. package/dist/modules/customers/ai-tools/deals-pack.js.map +1 -1
  68. package/dist/modules/customers/ai-tools/people-pack.js.map +1 -1
  69. package/dist/modules/customers/api/companies/route.js +4 -4
  70. package/dist/modules/customers/api/companies/route.js.map +2 -2
  71. package/dist/modules/customers/api/deals/route.js +43 -2
  72. package/dist/modules/customers/api/deals/route.js.map +2 -2
  73. package/dist/modules/customers/api/deals/summary/route.js +402 -0
  74. package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
  75. package/dist/modules/customers/api/people/route.js +4 -4
  76. package/dist/modules/customers/api/people/route.js.map +2 -2
  77. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
  78. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
  79. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
  80. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
  81. package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
  82. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  83. package/dist/modules/customers/backend/customers/deals/page.js +221 -56
  84. package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
  85. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
  86. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  87. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +18 -0
  88. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  89. package/dist/modules/customers/cli.js +15 -9
  90. package/dist/modules/customers/cli.js.map +2 -2
  91. package/dist/modules/customers/commands/addresses.js +5 -5
  92. package/dist/modules/customers/commands/addresses.js.map +2 -2
  93. package/dist/modules/customers/commands/comments.js +5 -5
  94. package/dist/modules/customers/commands/comments.js.map +2 -2
  95. package/dist/modules/customers/commands/deals.js +2 -2
  96. package/dist/modules/customers/commands/deals.js.map +2 -2
  97. package/dist/modules/customers/commands/entity-roles.js +2 -1
  98. package/dist/modules/customers/commands/entity-roles.js.map +2 -2
  99. package/dist/modules/customers/commands/interactions.js +8 -5
  100. package/dist/modules/customers/commands/interactions.js.map +2 -2
  101. package/dist/modules/customers/commands/shared.js +21 -6
  102. package/dist/modules/customers/commands/shared.js.map +2 -2
  103. package/dist/modules/customers/commands/tags.js +3 -3
  104. package/dist/modules/customers/commands/tags.js.map +2 -2
  105. package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
  106. package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
  107. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
  108. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
  109. package/dist/modules/customers/components/detail/DealForm.js +100 -17
  110. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  111. package/dist/modules/customers/components/detail/PersonDetailTabs.js +11 -3
  112. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  113. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
  114. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  115. package/dist/modules/customers/components/detail/assignableStaff.js +21 -8
  116. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  117. package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
  118. package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
  119. package/dist/modules/customers/lib/dealsMetrics.js +82 -0
  120. package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
  121. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +1 -1
  122. package/dist/modules/data_sync/api/run.js +1 -1
  123. package/dist/modules/data_sync/api/run.js.map +2 -2
  124. package/dist/modules/directory/api/organization-branding/route.js +214 -0
  125. package/dist/modules/directory/api/organization-branding/route.js.map +7 -0
  126. package/dist/modules/directory/api/organizations/route.js +7 -0
  127. package/dist/modules/directory/api/organizations/route.js.map +3 -3
  128. package/dist/modules/directory/backend/directory/branding/page.js +214 -0
  129. package/dist/modules/directory/backend/directory/branding/page.js.map +7 -0
  130. package/dist/modules/directory/backend/directory/branding/page.meta.js +26 -0
  131. package/dist/modules/directory/backend/directory/branding/page.meta.js.map +7 -0
  132. package/dist/modules/directory/commands/organizations.js +8 -1
  133. package/dist/modules/directory/commands/organizations.js.map +2 -2
  134. package/dist/modules/directory/data/entities.js +3 -0
  135. package/dist/modules/directory/data/entities.js.map +2 -2
  136. package/dist/modules/directory/data/validators.js +9 -0
  137. package/dist/modules/directory/data/validators.js.map +2 -2
  138. package/dist/modules/directory/migrations/Migration20260607222259_directory.js +13 -0
  139. package/dist/modules/directory/migrations/Migration20260607222259_directory.js.map +7 -0
  140. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
  141. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
  142. package/dist/modules/directory/utils/organizationScope.js +59 -27
  143. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  144. package/dist/modules/entities/api/definitions.batch.js +2 -1
  145. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  146. package/dist/modules/entities/api/entities.js +7 -0
  147. package/dist/modules/entities/api/entities.js.map +2 -2
  148. package/dist/modules/entities/api/records.js +26 -15
  149. package/dist/modules/entities/api/records.js.map +2 -2
  150. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
  151. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
  152. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
  153. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
  154. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
  155. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  156. package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
  157. package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
  158. package/dist/modules/payment_gateways/api/transactions/route.js +2 -4
  159. package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
  160. package/dist/modules/progress/api/jobs/[id]/route.js +7 -2
  161. package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
  162. package/dist/modules/progress/api/jobs/route.js +1 -1
  163. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  164. package/dist/modules/progress/lib/progressServiceImpl.js +8 -2
  165. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  166. package/dist/modules/query_index/data/entities.js +2 -1
  167. package/dist/modules/query_index/data/entities.js.map +2 -2
  168. package/dist/modules/query_index/lib/engine.js +4 -2
  169. package/dist/modules/query_index/lib/engine.js.map +2 -2
  170. package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js +16 -0
  171. package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js.map +7 -0
  172. package/dist/modules/resources/api/resources.js +2 -3
  173. package/dist/modules/resources/api/resources.js.map +2 -2
  174. package/dist/modules/sales/api/documents/factory.js +2 -2
  175. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  176. package/dist/modules/sales/commands/documents.js +7 -5
  177. package/dist/modules/sales/commands/documents.js.map +2 -2
  178. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +2 -1
  179. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  180. package/dist/modules/sales/components/documents/salesDocumentsColumns.js +10 -0
  181. package/dist/modules/sales/components/documents/salesDocumentsColumns.js.map +7 -0
  182. package/dist/modules/staff/api/team-members.js +9 -2
  183. package/dist/modules/staff/api/team-members.js.map +2 -2
  184. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
  185. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
  186. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
  187. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  188. package/dist/modules/staff/commands/team-members.js +1 -1
  189. package/dist/modules/staff/commands/team-members.js.map +2 -2
  190. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  191. package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
  192. package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
  193. package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
  194. package/dist/modules/sync_excel/api/import/route.js +1 -1
  195. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  196. package/dist/modules/workflows/api/definitions/route.js +3 -2
  197. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  198. package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
  199. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  200. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
  201. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  202. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
  203. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
  204. package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
  205. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  206. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
  207. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
  208. package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
  209. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  210. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
  211. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
  212. package/generated/entities/organization/index.ts +1 -0
  213. package/generated/entity-fields-registry.ts +1 -0
  214. package/package.json +11 -12
  215. package/src/bootstrap.ts +65 -7
  216. package/src/helpers/integration/crmFixtures.ts +21 -1
  217. package/src/modules/attachments/AGENTS.md +79 -0
  218. package/src/modules/attachments/api/library/route.ts +2 -2
  219. package/src/modules/attachments/api/route.ts +2 -0
  220. package/src/modules/attachments/components/AttachmentContentPreview.tsx +6 -6
  221. package/src/modules/attachments/lib/access.ts +36 -0
  222. package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +14 -2
  223. package/src/modules/audit_logs/data/entities.ts +1 -0
  224. package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +10 -0
  225. package/src/modules/audit_logs/migrations/Migration20260611104500.ts +13 -0
  226. package/src/modules/audit_logs/services/accessLogService.ts +15 -0
  227. package/src/modules/auth/api/admin/nav.ts +9 -0
  228. package/src/modules/auth/api/login.ts +13 -13
  229. package/src/modules/auth/commands/users.ts +32 -15
  230. package/src/modules/auth/data/entities.ts +13 -1
  231. package/src/modules/auth/i18n/de.json +0 -1
  232. package/src/modules/auth/i18n/en.json +0 -1
  233. package/src/modules/auth/i18n/es.json +0 -1
  234. package/src/modules/auth/i18n/pl.json +0 -1
  235. package/src/modules/auth/lib/backendChrome.tsx +37 -1
  236. package/src/modules/auth/lib/consentIntegrity.ts +6 -3
  237. package/src/modules/auth/migrations/.snapshot-open-mercato.json +20 -10
  238. package/src/modules/auth/migrations/Migration20260610120000.ts +53 -0
  239. package/src/modules/auth/migrations/Migration20260611103000.ts +21 -0
  240. package/src/modules/auth/services/authService.ts +24 -4
  241. package/src/modules/auth/services/rbacService.ts +11 -2
  242. package/src/modules/catalog/ai-tools/configuration-pack.ts +1 -1
  243. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +1 -1
  244. package/src/modules/catalog/ai-tools/products-pack.ts +1 -1
  245. package/src/modules/catalog/ai-tools/variants-pack.ts +1 -1
  246. package/src/modules/communication_channels/data/entities.ts +2 -2
  247. package/src/modules/communication_channels/encryption.ts +1 -1
  248. package/src/modules/communication_channels/lib/adapter.ts +1 -1
  249. package/src/modules/communication_channels/lib/thread-matcher.ts +1 -1
  250. package/src/modules/communication_channels/lib/thread-token.ts +1 -1
  251. package/src/modules/currencies/api/currencies/route.ts +4 -3
  252. package/src/modules/customer_accounts/api/admin/roles.ts +2 -1
  253. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
  254. package/src/modules/customer_accounts/events.ts +1 -1
  255. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +2 -2
  256. package/src/modules/customers/acl.ts +1 -1
  257. package/src/modules/customers/ai-tools/companies-pack.ts +1 -1
  258. package/src/modules/customers/ai-tools/deals-pack.ts +1 -1
  259. package/src/modules/customers/ai-tools/people-pack.ts +1 -1
  260. package/src/modules/customers/api/companies/route.ts +4 -4
  261. package/src/modules/customers/api/deals/route.ts +51 -2
  262. package/src/modules/customers/api/deals/summary/route.ts +496 -0
  263. package/src/modules/customers/api/people/route.ts +4 -4
  264. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
  265. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
  266. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
  267. package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
  268. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
  269. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -0
  270. package/src/modules/customers/cli.ts +15 -15
  271. package/src/modules/customers/commands/addresses.ts +5 -5
  272. package/src/modules/customers/commands/comments.ts +5 -5
  273. package/src/modules/customers/commands/deals.ts +2 -2
  274. package/src/modules/customers/commands/entity-roles.ts +2 -1
  275. package/src/modules/customers/commands/interactions.ts +8 -5
  276. package/src/modules/customers/commands/shared.ts +26 -4
  277. package/src/modules/customers/commands/tags.ts +3 -3
  278. package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
  279. package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
  280. package/src/modules/customers/components/detail/DealForm.tsx +121 -19
  281. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +12 -2
  282. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
  283. package/src/modules/customers/components/detail/assignableStaff.ts +32 -8
  284. package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
  285. package/src/modules/customers/i18n/de.json +43 -0
  286. package/src/modules/customers/i18n/en.json +43 -0
  287. package/src/modules/customers/i18n/es.json +43 -0
  288. package/src/modules/customers/i18n/pl.json +43 -0
  289. package/src/modules/customers/lib/dealsMetrics.ts +159 -0
  290. package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +1 -1
  291. package/src/modules/data_sync/api/run.ts +1 -1
  292. package/src/modules/directory/api/organization-branding/route.ts +238 -0
  293. package/src/modules/directory/api/organizations/route.ts +7 -0
  294. package/src/modules/directory/backend/directory/branding/page.meta.ts +24 -0
  295. package/src/modules/directory/backend/directory/branding/page.tsx +248 -0
  296. package/src/modules/directory/commands/organizations.ts +9 -1
  297. package/src/modules/directory/data/entities.ts +3 -0
  298. package/src/modules/directory/data/validators.ts +12 -0
  299. package/src/modules/directory/i18n/de.json +21 -0
  300. package/src/modules/directory/i18n/en.json +21 -0
  301. package/src/modules/directory/i18n/es.json +21 -0
  302. package/src/modules/directory/i18n/pl.json +21 -0
  303. package/src/modules/directory/migrations/.snapshot-open-mercato.json +40 -0
  304. package/src/modules/directory/migrations/Migration20260607222259_directory.ts +13 -0
  305. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
  306. package/src/modules/directory/utils/organizationScope.ts +85 -30
  307. package/src/modules/entities/api/definitions.batch.ts +11 -7
  308. package/src/modules/entities/api/entities.ts +11 -0
  309. package/src/modules/entities/api/records.ts +46 -25
  310. package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
  311. package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
  312. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
  313. package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
  314. package/src/modules/entities/i18n/de.json +1 -0
  315. package/src/modules/entities/i18n/en.json +1 -0
  316. package/src/modules/entities/i18n/es.json +1 -0
  317. package/src/modules/entities/i18n/pl.json +1 -0
  318. package/src/modules/payment_gateways/api/transactions/route.ts +2 -5
  319. package/src/modules/progress/api/jobs/[id]/route.ts +6 -1
  320. package/src/modules/progress/api/jobs/route.ts +1 -1
  321. package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
  322. package/src/modules/query_index/data/entities.ts +1 -0
  323. package/src/modules/query_index/lib/engine.ts +11 -5
  324. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +11 -0
  325. package/src/modules/query_index/migrations/Migration20260611103000_query_index.ts +29 -0
  326. package/src/modules/resources/api/resources.ts +2 -3
  327. package/src/modules/sales/api/documents/factory.ts +2 -2
  328. package/src/modules/sales/commands/documents.ts +7 -5
  329. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +2 -1
  330. package/src/modules/sales/components/documents/salesDocumentsColumns.ts +6 -0
  331. package/src/modules/staff/AGENTS.md +1 -1
  332. package/src/modules/staff/api/team-members.ts +9 -2
  333. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
  334. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
  335. package/src/modules/staff/commands/team-members.ts +5 -2
  336. package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
  337. package/src/modules/staff/i18n/de.json +1 -0
  338. package/src/modules/staff/i18n/en.json +1 -0
  339. package/src/modules/staff/i18n/es.json +1 -0
  340. package/src/modules/staff/i18n/pl.json +1 -0
  341. package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
  342. package/src/modules/sync_excel/api/import/route.ts +1 -1
  343. package/src/modules/workflows/api/definitions/route.ts +3 -2
  344. package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
  345. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
  346. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
  347. package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
  348. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
  349. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
  350. package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/communication_channels/lib/thread-token.ts"],
4
- "sourcesContent": ["import { createHash, createHmac, randomBytes, timingSafeEqual } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelThreadToken } from '../data/entities'\nimport { isUniqueViolation } from './pg-errors'\n\n/**\n * Per-thread crypto token used by the layered thread-matcher to attach\n * inbound replies to the originating Open Mercato message thread, even\n * when the recipient's mail client strips RFC 5322 headers.\n *\n * Token format: `om_<22b64url>_<11b64url>` \u2014 16 random bytes followed by\n * 8 bytes of HMAC-SHA256(random, key), each base64url-encoded without\n * padding. Approximately 37 characters total.\n *\n * Tokens are stored on the `channel_thread_tokens` table keyed by\n * `(tenantId, token)` so that even if the HMAC key leaked, tenant\n * isolation still holds at the DB layer.\n *\n * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`.\n */\n\nconst TOKEN_PREFIX = 'om_'\nconst RANDOM_BYTES = 16\nconst HMAC_BYTES = 8\nconst HMAC_KEY_ENV = 'OM_THREAD_TOKEN_SECRET'\nconst HMAC_FALLBACK_KEY_ENV = 'KMS_MASTER_KEY'\nconst HMAC_KEY_INFO = 'thread-token'\n\n/**\n * Pre-validated regex for parsing token candidates extracted from headers\n * or body content. Matches our exact format and rejects anything else\n * before HMAC verification \u2014 defense in depth.\n */\nconst TOKEN_REGEX = /om_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{11}/\n\nlet cachedKey: Buffer | null = null\n\n/** Resolve the HMAC key. Falls back through env vars per the spec. */\nfunction getKey(): Buffer {\n if (cachedKey) return cachedKey\n const primary = process.env[HMAC_KEY_ENV]\n if (primary && primary.length > 0) {\n cachedKey = Buffer.from(primary, 'utf8')\n return cachedKey\n }\n const fallback = process.env[HMAC_FALLBACK_KEY_ENV]\n if (fallback && fallback.length > 0) {\n // HKDF-style: derive a per-purpose subkey by HMAC-ing the fallback secret\n // with a constant info label so different purposes don't share a key.\n cachedKey = createHmac('sha256', fallback).update(HMAC_KEY_INFO).digest()\n return cachedKey\n }\n // No secret configured. Fail closed in production rather than signing thread\n // tokens with a public static key (which would let anyone forge a thread\n // token). In non-production we fall back to a dev-only static key and warn.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured \u2014` +\n ' refusing to sign thread tokens with a static dev key in production.',\n )\n }\n console.warn(\n `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured.` +\n ' Thread tokens will use a dev-only static key \u2014 DO NOT USE IN PRODUCTION.',\n )\n cachedKey = createHash('sha256').update('open-mercato-thread-token-dev').digest()\n return cachedKey\n}\n\n/** Reset the cached key \u2014 for tests that mutate env vars. */\nexport function _resetThreadTokenKeyCache(): void {\n cachedKey = null\n}\n\nfunction base64urlEncode(buf: Buffer): string {\n return buf\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/g, '')\n}\n\nfunction base64urlDecode(value: string): Buffer | null {\n if (!/^[A-Za-z0-9_-]+$/.test(value)) return null\n const padded = value + '='.repeat((4 - (value.length % 4)) % 4)\n try {\n return Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64')\n } catch {\n return null\n }\n}\n\nfunction computeHmacBytes(random: Buffer): Buffer {\n return createHmac('sha256', getKey()).update(random).digest().subarray(0, HMAC_BYTES)\n}\n\n/**\n * Generate a new HMAC-signed thread token. The 16 random bytes make a token\n * collision astronomically unlikely; the `(tenantId, token)` unique constraint\n * is the backstop. Per-thread deduplication (one token per thread) is handled\n * separately by `getOrCreateThreadToken` via the `(tenantId, messageThreadId)`\n * unique constraint \u2014 not here.\n */\nexport function generateToken(): string {\n const random = randomBytes(RANDOM_BYTES)\n const hmac = computeHmacBytes(random)\n return `${TOKEN_PREFIX}${base64urlEncode(random)}_${base64urlEncode(hmac)}`\n}\n\n// Fixed lengths of the base64url-encoded components, without padding.\n// Computed once: 16 bytes -> 22 chars, 8 bytes -> 11 chars.\nconst RANDOM_B64_LEN = Math.ceil((RANDOM_BYTES * 4) / 3)\nconst HMAC_B64_LEN = Math.ceil((HMAC_BYTES * 4) / 3)\nconst TOKEN_TOTAL_LEN = TOKEN_PREFIX.length + RANDOM_B64_LEN + 1 + HMAC_B64_LEN\n\n/**\n * Verify the HMAC signature on a token. Returns `true` only when the\n * structural form is correct AND the HMAC matches under the current key.\n *\n * Does NOT verify the token exists in the DB \u2014 that lookup is the\n * matcher's responsibility (see `thread-matcher.ts`). Verifying here\n * lets us drop forged tokens before any DB I/O.\n *\n * Parsing note: base64url-encoded random/HMAC portions may themselves\n * contain `_` characters, so `split('_')` is unsafe. We parse positionally\n * using the fixed lengths declared above.\n */\nexport function verifyToken(token: string): boolean {\n if (typeof token !== 'string') return false\n if (token.length !== TOKEN_TOTAL_LEN) return false\n if (!token.startsWith(TOKEN_PREFIX)) return false\n const randomStart = TOKEN_PREFIX.length\n const randomEnd = randomStart + RANDOM_B64_LEN\n const separator = token[randomEnd]\n if (separator !== '_') return false\n const hmacStart = randomEnd + 1\n const randomPart = token.slice(randomStart, randomEnd)\n const hmacPart = token.slice(hmacStart, hmacStart + HMAC_B64_LEN)\n const random = base64urlDecode(randomPart)\n if (!random || random.length !== RANDOM_BYTES) return false\n const provided = base64urlDecode(hmacPart)\n if (!provided || provided.length !== HMAC_BYTES) return false\n const expected = computeHmacBytes(random)\n try {\n return timingSafeEqual(provided, expected)\n } catch {\n return false\n }\n}\n\n/**\n * Build the synthetic RFC 5322 Message-ID we inject into outbound\n * `References:` headers. Uses the IANA-reserved `.invalid` TLD (RFC 6761\n * \u00A7 3) so RFC-compliant MTAs MUST accept it as syntactically valid.\n */\nexport function buildReferencesId(token: string): string {\n return `<${token}@open-mercato.invalid>`\n}\n\n/**\n * Build the hidden HTML body span + plain-text trailer used as the\n * token's secondary attachment point (in case `References` is stripped\n * by the recipient's MUA).\n */\nexport function buildBodyFooter(token: string): { html: string; plain: string } {\n return {\n html: `<span style=\"display:none\">[OM:${token}]</span>`,\n plain: `\\n\\n[OM:${token}]`,\n }\n}\n\n/**\n * Apply the thread token to an outbound MIME-like payload. Mutates the\n * input shape minimally and idempotently:\n * - `headers.references`: appends the synthetic `<om_TOKEN@\u2026>` id if not\n * already present (deduped).\n * - `bodyHtml`: injects a hidden `<span>` before the last `</body>` tag,\n * or appends if no `</body>` is present.\n * - `bodyText`: appends the plain-text trailer.\n *\n * Returns a NEW object \u2014 does not mutate the input. Callers that maintain\n * their own MIME structure can call the building blocks directly.\n */\nexport function applyOutboundThreadingToken<\n T extends {\n headers?: Record<string, string | string[] | undefined>\n bodyHtml?: string\n bodyText?: string\n },\n>(payload: T, token: string): T {\n if (!verifyToken(token)) {\n throw new Error('applyOutboundThreadingToken: invalid token format/signature')\n }\n const refId = buildReferencesId(token)\n const footer = buildBodyFooter(token)\n\n const headers = { ...(payload.headers ?? {}) } as Record<string, string | string[] | undefined>\n const existingRefs = headers['references'] ?? headers['References']\n let nextRefs: string\n if (Array.isArray(existingRefs)) {\n nextRefs = existingRefs.includes(refId) ? existingRefs.join(' ') : [...existingRefs, refId].join(' ')\n } else if (typeof existingRefs === 'string' && existingRefs.length > 0) {\n nextRefs = existingRefs.includes(refId) ? existingRefs : `${existingRefs} ${refId}`\n } else {\n nextRefs = refId\n }\n // Normalise to the canonical RFC 5322 header name and drop any duplicate\n // lowercase entry so the MTA sees a single `References` header.\n delete headers['references']\n headers['References'] = nextRefs\n\n let bodyHtml = payload.bodyHtml\n if (typeof bodyHtml === 'string') {\n if (!bodyHtml.includes(`[OM:${token}]`)) {\n const closing = bodyHtml.lastIndexOf('</body>')\n if (closing >= 0) {\n bodyHtml = `${bodyHtml.slice(0, closing)}${footer.html}${bodyHtml.slice(closing)}`\n } else {\n bodyHtml = `${bodyHtml}${footer.html}`\n }\n }\n }\n\n let bodyText = payload.bodyText\n if (typeof bodyText === 'string') {\n if (!bodyText.includes(`[OM:${token}]`)) {\n bodyText = `${bodyText}${footer.plain}`\n }\n }\n\n return {\n ...payload,\n headers,\n ...(bodyHtml !== undefined ? { bodyHtml } : {}),\n ...(bodyText !== undefined ? { bodyText } : {}),\n }\n}\n\n/**\n * Extract token candidates from a `References` / `In-Reply-To` header\n * value (string or string[]) and return the FIRST one that HMAC-verifies.\n * Returns `null` if no valid token is present.\n */\nexport function extractTokenFromHeaders(\n inReplyTo: string | null | undefined,\n references: string[] | string | null | undefined,\n): string | null {\n const haystack: string[] = []\n if (typeof inReplyTo === 'string' && inReplyTo.length > 0) haystack.push(inReplyTo)\n if (Array.isArray(references)) haystack.push(...references)\n else if (typeof references === 'string' && references.length > 0) haystack.push(references)\n for (const candidate of haystack) {\n const matches = candidate.match(new RegExp(TOKEN_REGEX, 'g'))\n if (!matches) continue\n for (const match of matches) {\n if (verifyToken(match)) return match\n }\n }\n return null\n}\n\n/**\n * Idempotent get-or-create: return the existing `ChannelThreadToken` for the\n * given thread, or create + return a new one. Idempotency is enforced by the\n * `channel_thread_tokens_thread_uq` unique constraint on\n * `(tenant_id, message_thread_id)`: a concurrent double-create loses the race\n * with a unique violation, which we catch and resolve by re-selecting the\n * winner \u2014 so callers always converge on exactly one token per thread.\n *\n * Reads via the standard EntityManager (no encryption needed \u2014 the token\n * column itself is the HMAC-signed value, not encrypted at rest).\n *\n * Use cases:\n * - Outbound subscriber: get or create a token before injecting it\n * into the outbound MIME (`applyOutboundThreadingToken`).\n * - Future \"reset\" UI for tenant admins: explicit rotation by deleting\n * the row + calling this helper again.\n */\nexport async function getOrCreateThreadToken(\n em: EntityManager,\n args: {\n tenantId: string\n organizationId: string | null\n messageThreadId: string\n },\n): Promise<{ token: string; created: boolean }> {\n const dscope = { tenantId: args.tenantId, organizationId: args.organizationId }\n const existing = await findOneWithDecryption(\n em,\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (existing) {\n return { token: existing.token, created: false }\n }\n const row = em.create(ChannelThreadToken, {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n token: generateToken(),\n })\n // MikroORM v7 removed `persistAndFlush` \u2014 split into persist + flush.\n em.persist(row)\n try {\n await em.flush()\n return { token: row.token, created: true }\n } catch (err) {\n // A concurrent create for the same (tenant, thread) won the race; the\n // unique constraint rejected ours. Re-select the winner on a clean fork so\n // we never return a half-persisted row or surface a spurious error.\n if (!isUniqueViolation(err)) throw err\n const winner = await findOneWithDecryption(\n em.fork(),\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (winner) return { token: winner.token, created: false }\n throw err\n }\n}\n\n/**\n * Extract a token candidate from an inbound body (HTML or plain text).\n * Scans for `[OM:om_\u2026]` markers and returns the first that HMAC-verifies.\n */\nexport function extractTokenFromBody(\n bodyHtml: string | null | undefined,\n bodyText: string | null | undefined,\n): string | null {\n const haystacks = [bodyHtml, bodyText].filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n )\n const pattern = new RegExp(`\\\\[OM:(${TOKEN_REGEX.source})\\\\]`, 'g')\n for (const haystack of haystacks) {\n let match: RegExpExecArray | null\n pattern.lastIndex = 0\n while ((match = pattern.exec(haystack)) !== null) {\n if (verifyToken(match[1])) return match[1]\n }\n }\n return null\n}\n"],
4
+ "sourcesContent": ["import { createHash, createHmac, randomBytes, timingSafeEqual } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelThreadToken } from '../data/entities'\nimport { isUniqueViolation } from './pg-errors'\n\n/**\n * Per-thread crypto token used by the layered thread-matcher to attach\n * inbound replies to the originating Open Mercato message thread, even\n * when the recipient's mail client strips RFC 5322 headers.\n *\n * Token format: `om_<22b64url>_<11b64url>` \u2014 16 random bytes followed by\n * 8 bytes of HMAC-SHA256(random, key), each base64url-encoded without\n * padding. Approximately 37 characters total.\n *\n * Tokens are stored on the `channel_thread_tokens` table keyed by\n * `(tenantId, token)` so that even if the HMAC key leaked, tenant\n * isolation still holds at the DB layer.\n *\n * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`.\n */\n\nconst TOKEN_PREFIX = 'om_'\nconst RANDOM_BYTES = 16\nconst HMAC_BYTES = 8\nconst HMAC_KEY_ENV = 'OM_THREAD_TOKEN_SECRET'\nconst HMAC_FALLBACK_KEY_ENV = 'KMS_MASTER_KEY'\nconst HMAC_KEY_INFO = 'thread-token'\n\n/**\n * Pre-validated regex for parsing token candidates extracted from headers\n * or body content. Matches our exact format and rejects anything else\n * before HMAC verification \u2014 defense in depth.\n */\nconst TOKEN_REGEX = /om_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{11}/\n\nlet cachedKey: Buffer | null = null\n\n/** Resolve the HMAC key. Falls back through env vars per the spec. */\nfunction getKey(): Buffer {\n if (cachedKey) return cachedKey\n const primary = process.env[HMAC_KEY_ENV]\n if (primary && primary.length > 0) {\n cachedKey = Buffer.from(primary, 'utf8')\n return cachedKey\n }\n const fallback = process.env[HMAC_FALLBACK_KEY_ENV]\n if (fallback && fallback.length > 0) {\n // HKDF-style: derive a per-purpose subkey by HMAC-ing the fallback secret\n // with a constant info label so different purposes don't share a key.\n cachedKey = createHmac('sha256', fallback).update(HMAC_KEY_INFO).digest()\n return cachedKey\n }\n // No secret configured. Fail closed in production rather than signing thread\n // tokens with a public static key (which would let anyone forge a thread\n // token). In non-production we fall back to a dev-only static key and warn.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured \u2014` +\n ' refusing to sign thread tokens with a static dev key in production.',\n )\n }\n console.warn(\n `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured.` +\n ' Thread tokens will use a dev-only static key \u2014 DO NOT USE IN PRODUCTION.',\n )\n cachedKey = createHash('sha256').update('open-mercato-thread-token-dev').digest()\n return cachedKey\n}\n\n/** Reset the cached key \u2014 for tests that mutate env vars. */\nexport function _resetThreadTokenKeyCache(): void {\n cachedKey = null\n}\n\nfunction base64urlEncode(buf: Buffer): string {\n return buf\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/g, '')\n}\n\nfunction base64urlDecode(value: string): Buffer | null {\n if (!/^[A-Za-z0-9_-]+$/.test(value)) return null\n const padded = value + '='.repeat((4 - (value.length % 4)) % 4)\n try {\n return Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64')\n } catch {\n return null\n }\n}\n\nfunction computeHmacBytes(random: Buffer): Buffer {\n return createHmac('sha256', getKey()).update(random).digest().subarray(0, HMAC_BYTES)\n}\n\n/**\n * Generate a new HMAC-signed thread token. The 16 random bytes make a token\n * collision astronomically unlikely; the `(tenantId, token)` unique constraint\n * is the backstop. Per-thread deduplication (one token per thread) is handled\n * separately by `getOrCreateThreadToken` via the `(tenantId, messageThreadId)`\n * unique constraint \u2014 not here.\n */\nexport function generateToken(): string {\n const random = randomBytes(RANDOM_BYTES)\n const hmac = computeHmacBytes(random)\n return `${TOKEN_PREFIX}${base64urlEncode(random)}_${base64urlEncode(hmac)}`\n}\n\n// Fixed lengths of the base64url-encoded components, without padding.\n// Computed once: 16 bytes -> 22 chars, 8 bytes -> 11 chars.\nconst RANDOM_B64_LEN = Math.ceil((RANDOM_BYTES * 4) / 3)\nconst HMAC_B64_LEN = Math.ceil((HMAC_BYTES * 4) / 3)\nconst TOKEN_TOTAL_LEN = TOKEN_PREFIX.length + RANDOM_B64_LEN + 1 + HMAC_B64_LEN\n\n/**\n * Verify the HMAC signature on a token. Returns `true` only when the\n * structural form is correct AND the HMAC matches under the current key.\n *\n * Does NOT verify the token exists in the DB \u2014 that lookup is the\n * matcher's responsibility (see `thread-matcher.ts`). Verifying here\n * lets us drop forged tokens before any DB I/O.\n *\n * Parsing note: base64url-encoded random/HMAC portions may themselves\n * contain `_` characters, so `split('_')` is unsafe. We parse positionally\n * using the fixed lengths declared above.\n */\nexport function verifyToken(token: string): boolean {\n if (typeof token !== 'string') return false\n if (token.length !== TOKEN_TOTAL_LEN) return false\n if (!token.startsWith(TOKEN_PREFIX)) return false\n const randomStart = TOKEN_PREFIX.length\n const randomEnd = randomStart + RANDOM_B64_LEN\n const separator = token[randomEnd]\n if (separator !== '_') return false\n const hmacStart = randomEnd + 1\n const randomPart = token.slice(randomStart, randomEnd)\n const hmacPart = token.slice(hmacStart, hmacStart + HMAC_B64_LEN)\n const random = base64urlDecode(randomPart)\n if (!random || random.length !== RANDOM_BYTES) return false\n const provided = base64urlDecode(hmacPart)\n if (!provided || provided.length !== HMAC_BYTES) return false\n const expected = computeHmacBytes(random)\n try {\n return timingSafeEqual(provided, expected)\n } catch {\n return false\n }\n}\n\n/**\n * Build the synthetic RFC 5322 Message-ID we inject into outbound\n * `References:` headers. Uses the IANA-reserved `.invalid` TLD (RFC 6761\n * \u00A7 3) so RFC-compliant MTAs MUST accept it as syntactically valid.\n */\nexport function buildReferencesId(token: string): string {\n return `<${token}@open-mercato.invalid>`\n}\n\n/**\n * Build the hidden HTML body span + plain-text trailer used as the\n * token's secondary attachment point (in case `References` is stripped\n * by the recipient's MUA).\n */\nexport function buildBodyFooter(token: string): { html: string; plain: string } {\n return {\n html: `<span style=\"display:none\">[OM:${token}]</span>`,\n plain: `\\n\\n[OM:${token}]`,\n }\n}\n\n/**\n * Apply the thread token to an outbound MIME-like payload. Mutates the\n * input shape minimally and idempotently:\n * - `headers.references`: appends the synthetic `<om_TOKEN@\u2026>` id if not\n * already present (deduped).\n * - `bodyHtml`: injects a hidden `<span>` before the last `</body>` tag,\n * or appends if no `</body>` is present.\n * - `bodyText`: appends the plain-text trailer.\n *\n * Returns a NEW object \u2014 does not mutate the input. Callers that maintain\n * their own MIME structure can call the building blocks directly.\n */\nexport function applyOutboundThreadingToken<\n T extends {\n headers?: Record<string, string | string[] | undefined>\n bodyHtml?: string\n bodyText?: string\n },\n>(payload: T, token: string): T {\n if (!verifyToken(token)) {\n throw new Error('applyOutboundThreadingToken: invalid token format/signature')\n }\n const refId = buildReferencesId(token)\n const footer = buildBodyFooter(token)\n\n const headers = { ...(payload.headers ?? {}) } as Record<string, string | string[] | undefined>\n const existingRefs = headers['references'] ?? headers['References']\n let nextRefs: string\n if (Array.isArray(existingRefs)) {\n nextRefs = existingRefs.includes(refId) ? existingRefs.join(' ') : [...existingRefs, refId].join(' ')\n } else if (typeof existingRefs === 'string' && existingRefs.length > 0) {\n nextRefs = existingRefs.includes(refId) ? existingRefs : `${existingRefs} ${refId}`\n } else {\n nextRefs = refId\n }\n // Normalise to the canonical RFC 5322 header name and drop any duplicate\n // lowercase entry so the MTA sees a single `References` header.\n delete headers['references']\n headers['References'] = nextRefs\n\n let bodyHtml = payload.bodyHtml\n if (typeof bodyHtml === 'string') {\n if (!bodyHtml.includes(`[OM:${token}]`)) {\n const closing = bodyHtml.lastIndexOf('</body>')\n if (closing >= 0) {\n bodyHtml = `${bodyHtml.slice(0, closing)}${footer.html}${bodyHtml.slice(closing)}`\n } else {\n bodyHtml = `${bodyHtml}${footer.html}`\n }\n }\n }\n\n let bodyText = payload.bodyText\n if (typeof bodyText === 'string') {\n if (!bodyText.includes(`[OM:${token}]`)) {\n bodyText = `${bodyText}${footer.plain}`\n }\n }\n\n return {\n ...payload,\n headers,\n ...(bodyHtml !== undefined ? { bodyHtml } : {}),\n ...(bodyText !== undefined ? { bodyText } : {}),\n }\n}\n\n/**\n * Extract token candidates from a `References` / `In-Reply-To` header\n * value (string or string[]) and return the FIRST one that HMAC-verifies.\n * Returns `null` if no valid token is present.\n */\nexport function extractTokenFromHeaders(\n inReplyTo: string | null | undefined,\n references: string[] | string | null | undefined,\n): string | null {\n const haystack: string[] = []\n if (typeof inReplyTo === 'string' && inReplyTo.length > 0) haystack.push(inReplyTo)\n if (Array.isArray(references)) haystack.push(...references)\n else if (typeof references === 'string' && references.length > 0) haystack.push(references)\n for (const candidate of haystack) {\n const matches = candidate.match(new RegExp(TOKEN_REGEX, 'g'))\n if (!matches) continue\n for (const match of matches) {\n if (verifyToken(match)) return match\n }\n }\n return null\n}\n\n/**\n * Idempotent get-or-create: return the existing `ChannelThreadToken` for the\n * given thread, or create + return a new one. Idempotency is enforced by the\n * `channel_thread_tokens_thread_uq` unique constraint on\n * `(tenant_id, message_thread_id)`: a concurrent double-create loses the race\n * with a unique violation, which we catch and resolve by re-selecting the\n * winner \u2014 so callers always converge on exactly one token per thread.\n *\n * Reads via the standard EntityManager (no encryption needed \u2014 the token\n * column itself is the HMAC-signed value, not encrypted at rest).\n *\n * Use cases:\n * - Outbound subscriber: get or create a token before injecting it\n * into the outbound MIME (`applyOutboundThreadingToken`).\n * - Future \"reset\" UI for tenant admins: explicit rotation by deleting\n * the row + calling this helper again.\n */\nexport async function getOrCreateThreadToken(\n em: EntityManager,\n args: {\n tenantId: string\n organizationId: string | null\n messageThreadId: string\n },\n): Promise<{ token: string; created: boolean }> {\n const dscope = { tenantId: args.tenantId, organizationId: args.organizationId }\n const existing = await findOneWithDecryption(\n em,\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (existing) {\n return { token: existing.token, created: false }\n }\n const row = em.create(ChannelThreadToken, {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n token: generateToken(),\n })\n // MikroORM v7 removed `persistAndFlush` \u2014 split into persist + flush.\n em.persist(row)\n try {\n await em.flush()\n return { token: row.token, created: true }\n } catch (err) {\n // A concurrent create for the same (tenant, thread) won the race; the\n // unique constraint rejected ours. Re-select the winner on a clean fork so\n // we never return a half-persisted row or surface a spurious error.\n if (!isUniqueViolation(err)) throw err\n const winner = await findOneWithDecryption(\n em.fork(),\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (winner) return { token: winner.token, created: false }\n throw err\n }\n}\n\n/**\n * Extract a token candidate from an inbound body (HTML or plain text).\n * Scans for `[OM:om_\u2026]` markers and returns the first that HMAC-verifies.\n */\nexport function extractTokenFromBody(\n bodyHtml: string | null | undefined,\n bodyText: string | null | undefined,\n): string | null {\n const haystacks = [bodyHtml, bodyText].filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n )\n const pattern = new RegExp(`\\\\[OM:(${TOKEN_REGEX.source})\\\\]`, 'g')\n for (const haystack of haystacks) {\n let match: RegExpExecArray | null\n pattern.lastIndex = 0\n while ((match = pattern.exec(haystack)) !== null) {\n if (verifyToken(match[1])) return match[1]\n }\n }\n return null\n}\n"],
5
5
  "mappings": "AAAA,SAAS,YAAY,YAAY,aAAa,uBAAuB;AAErE,SAAS,6BAA6B;AACtC,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAkBlC,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,aAAa;AACnB,MAAM,eAAe;AACrB,MAAM,wBAAwB;AAC9B,MAAM,gBAAgB;AAOtB,MAAM,cAAc;AAEpB,IAAI,YAA2B;AAG/B,SAAS,SAAiB;AACxB,MAAI,UAAW,QAAO;AACtB,QAAM,UAAU,QAAQ,IAAI,YAAY;AACxC,MAAI,WAAW,QAAQ,SAAS,GAAG;AACjC,gBAAY,OAAO,KAAK,SAAS,MAAM;AACvC,WAAO;AAAA,EACT;AACA,QAAM,WAAW,QAAQ,IAAI,qBAAqB;AAClD,MAAI,YAAY,SAAS,SAAS,GAAG;AAGnC,gBAAY,WAAW,UAAU,QAAQ,EAAE,OAAO,aAAa,EAAE,OAAO;AACxE,WAAO;AAAA,EACT;AAIA,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,UAAM,IAAI;AAAA,MACR,+BAA+B,YAAY,OAAO,qBAAqB;AAAA,IAEzE;AAAA,EACF;AACA,UAAQ;AAAA,IACN,+BAA+B,YAAY,OAAO,qBAAqB;AAAA,EAEzE;AACA,cAAY,WAAW,QAAQ,EAAE,OAAO,+BAA+B,EAAE,OAAO;AAChF,SAAO;AACT;AAGO,SAAS,4BAAkC;AAChD,cAAY;AACd;AAEA,SAAS,gBAAgB,KAAqB;AAC5C,SAAO,IACJ,SAAS,QAAQ,EACjB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,QAAQ,EAAE;AACvB;AAEA,SAAS,gBAAgB,OAA8B;AACrD,MAAI,CAAC,mBAAmB,KAAK,KAAK,EAAG,QAAO;AAC5C,QAAM,SAAS,QAAQ,IAAI,QAAQ,IAAK,MAAM,SAAS,KAAM,CAAC;AAC9D,MAAI;AACF,WAAO,OAAO,KAAK,OAAO,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,GAAG,QAAQ;AAAA,EAC3E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,iBAAiB,QAAwB;AAChD,SAAO,WAAW,UAAU,OAAO,CAAC,EAAE,OAAO,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,UAAU;AACtF;AASO,SAAS,gBAAwB;AACtC,QAAM,SAAS,YAAY,YAAY;AACvC,QAAM,OAAO,iBAAiB,MAAM;AACpC,SAAO,GAAG,YAAY,GAAG,gBAAgB,MAAM,CAAC,IAAI,gBAAgB,IAAI,CAAC;AAC3E;AAIA,MAAM,iBAAiB,KAAK,KAAM,eAAe,IAAK,CAAC;AACvD,MAAM,eAAe,KAAK,KAAM,aAAa,IAAK,CAAC;AACnD,MAAM,kBAAkB,aAAa,SAAS,iBAAiB,IAAI;AAc5D,SAAS,YAAY,OAAwB;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,MAAM,WAAW,gBAAiB,QAAO;AAC7C,MAAI,CAAC,MAAM,WAAW,YAAY,EAAG,QAAO;AAC5C,QAAM,cAAc,aAAa;AACjC,QAAM,YAAY,cAAc;AAChC,QAAM,YAAY,MAAM,SAAS;AACjC,MAAI,cAAc,IAAK,QAAO;AAC9B,QAAM,YAAY,YAAY;AAC9B,QAAM,aAAa,MAAM,MAAM,aAAa,SAAS;AACrD,QAAM,WAAW,MAAM,MAAM,WAAW,YAAY,YAAY;AAChE,QAAM,SAAS,gBAAgB,UAAU;AACzC,MAAI,CAAC,UAAU,OAAO,WAAW,aAAc,QAAO;AACtD,QAAM,WAAW,gBAAgB,QAAQ;AACzC,MAAI,CAAC,YAAY,SAAS,WAAW,WAAY,QAAO;AACxD,QAAM,WAAW,iBAAiB,MAAM;AACxC,MAAI;AACF,WAAO,gBAAgB,UAAU,QAAQ;AAAA,EAC3C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,SAAS,kBAAkB,OAAuB;AACvD,SAAO,IAAI,KAAK;AAClB;AAOO,SAAS,gBAAgB,OAAgD;AAC9E,SAAO;AAAA,IACL,MAAM,kCAAkC,KAAK;AAAA,IAC7C,OAAO;AAAA;AAAA,MAAW,KAAK;AAAA,EACzB;AACF;AAcO,SAAS,4BAMd,SAAY,OAAkB;AAC9B,MAAI,CAAC,YAAY,KAAK,GAAG;AACvB,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AACA,QAAM,QAAQ,kBAAkB,KAAK;AACrC,QAAM,SAAS,gBAAgB,KAAK;AAEpC,QAAM,UAAU,EAAE,GAAI,QAAQ,WAAW,CAAC,EAAG;AAC7C,QAAM,eAAe,QAAQ,YAAY,KAAK,QAAQ,YAAY;AAClE,MAAI;AACJ,MAAI,MAAM,QAAQ,YAAY,GAAG;AAC/B,eAAW,aAAa,SAAS,KAAK,IAAI,aAAa,KAAK,GAAG,IAAI,CAAC,GAAG,cAAc,KAAK,EAAE,KAAK,GAAG;AAAA,EACtG,WAAW,OAAO,iBAAiB,YAAY,aAAa,SAAS,GAAG;AACtE,eAAW,aAAa,SAAS,KAAK,IAAI,eAAe,GAAG,YAAY,IAAI,KAAK;AAAA,EACnF,OAAO;AACL,eAAW;AAAA,EACb;AAGA,SAAO,QAAQ,YAAY;AAC3B,UAAQ,YAAY,IAAI;AAExB,MAAI,WAAW,QAAQ;AACvB,MAAI,OAAO,aAAa,UAAU;AAChC,QAAI,CAAC,SAAS,SAAS,OAAO,KAAK,GAAG,GAAG;AACvC,YAAM,UAAU,SAAS,YAAY,SAAS;AAC9C,UAAI,WAAW,GAAG;AAChB,mBAAW,GAAG,SAAS,MAAM,GAAG,OAAO,CAAC,GAAG,OAAO,IAAI,GAAG,SAAS,MAAM,OAAO,CAAC;AAAA,MAClF,OAAO;AACL,mBAAW,GAAG,QAAQ,GAAG,OAAO,IAAI;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAEA,MAAI,WAAW,QAAQ;AACvB,MAAI,OAAO,aAAa,UAAU;AAChC,QAAI,CAAC,SAAS,SAAS,OAAO,KAAK,GAAG,GAAG;AACvC,iBAAW,GAAG,QAAQ,GAAG,OAAO,KAAK;AAAA,IACvC;AAAA,EACF;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,IAC7C,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,EAC/C;AACF;AAOO,SAAS,wBACd,WACA,YACe;AACf,QAAM,WAAqB,CAAC;AAC5B,MAAI,OAAO,cAAc,YAAY,UAAU,SAAS,EAAG,UAAS,KAAK,SAAS;AAClF,MAAI,MAAM,QAAQ,UAAU,EAAG,UAAS,KAAK,GAAG,UAAU;AAAA,WACjD,OAAO,eAAe,YAAY,WAAW,SAAS,EAAG,UAAS,KAAK,UAAU;AAC1F,aAAW,aAAa,UAAU;AAChC,UAAM,UAAU,UAAU,MAAM,IAAI,OAAO,aAAa,GAAG,CAAC;AAC5D,QAAI,CAAC,QAAS;AACd,eAAW,SAAS,SAAS;AAC3B,UAAI,YAAY,KAAK,EAAG,QAAO;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;AAmBA,eAAsB,uBACpB,IACA,MAK8C;AAC9C,QAAM,SAAS,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,eAAe;AAC9E,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,iBAAiB,KAAK;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,UAAU;AACZ,WAAO,EAAE,OAAO,SAAS,OAAO,SAAS,MAAM;AAAA,EACjD;AACA,QAAM,MAAM,GAAG,OAAO,oBAAoB;AAAA,IACxC,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,IACrB,iBAAiB,KAAK;AAAA,IACtB,OAAO,cAAc;AAAA,EACvB,CAAC;AAED,KAAG,QAAQ,GAAG;AACd,MAAI;AACF,UAAM,GAAG,MAAM;AACf,WAAO,EAAE,OAAO,IAAI,OAAO,SAAS,KAAK;AAAA,EAC3C,SAAS,KAAK;AAIZ,QAAI,CAAC,kBAAkB,GAAG,EAAG,OAAM;AACnC,UAAM,SAAS,MAAM;AAAA,MACnB,GAAG,KAAK;AAAA,MACR;AAAA,MACA;AAAA,QACE,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK;AAAA,QACrB,iBAAiB,KAAK;AAAA,MACxB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,QAAI,OAAQ,QAAO,EAAE,OAAO,OAAO,OAAO,SAAS,MAAM;AACzD,UAAM;AAAA,EACR;AACF;AAMO,SAAS,qBACd,UACA,UACe;AACf,QAAM,YAAY,CAAC,UAAU,QAAQ,EAAE;AAAA,IACrC,CAAC,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS;AAAA,EAC1E;AACA,QAAM,UAAU,IAAI,OAAO,UAAU,YAAY,MAAM,QAAQ,GAAG;AAClE,aAAW,YAAY,WAAW;AAChC,QAAI;AACJ,YAAQ,YAAY;AACpB,YAAQ,QAAQ,QAAQ,KAAK,QAAQ,OAAO,MAAM;AAChD,UAAI,YAAY,MAAM,CAAC,CAAC,EAAG,QAAO,MAAM,CAAC;AAAA,IAC3C;AAAA,EACF;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -4,6 +4,7 @@ import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
4
4
  import { Currency } from "../../data/entities.js";
5
5
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
6
6
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
7
+ import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
7
8
  import { currencyCreateSchema, currencyUpdateSchema } from "../../data/validators.js";
8
9
  import {
9
10
  createCurrenciesCrudOpenApi,
@@ -118,9 +119,9 @@ async function GET(req) {
118
119
  if (code) filter.code = code;
119
120
  if (search) {
120
121
  filter.$or = [
121
- { code: { $ilike: `%${search}%` } },
122
- { name: { $ilike: `%${search}%` } },
123
- { symbol: { $ilike: `%${search}%` } }
122
+ { code: { $ilike: `%${escapeLikePattern(search)}%` } },
123
+ { name: { $ilike: `%${escapeLikePattern(search)}%` } },
124
+ { symbol: { $ilike: `%${escapeLikePattern(search)}%` } }
124
125
  ];
125
126
  }
126
127
  if (isBase === "true") filter.isBase = true;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/currencies/api/currencies/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { Currency } from '../../data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { currencyCreateSchema, currencyUpdateSchema } from '../../data/validators'\nimport {\n createCurrenciesCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['currencies.view'] },\n POST: { requireAuth: true, requireFeatures: ['currencies.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['currencies.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['currencies.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).loose()\ntype CrudInput = Record<string, unknown>\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: Currency,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n events: {\n module: 'currencies',\n entity: 'currency',\n persistent: true,\n },\n actions: {\n create: {\n commandId: 'currencies.currencies.create',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: ({ result }) => ({ id: String(result.currencyId) }),\n status: 201,\n },\n update: {\n commandId: 'currencies.currencies.update',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'currencies.currencies.delete',\n schema: rawBodySchema,\n mapInput: ({ raw, ctx }) => ({\n id: ((raw as Record<string, unknown>).query as Record<string, unknown> | undefined)?.id as string | undefined,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? undefined,\n tenantId: ctx.auth?.tenantId ?? undefined,\n }),\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst listQuerySchema = z.object({\n id: z.uuid().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n sortField: z.enum(['code', 'name', 'createdAt', 'updatedAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n isBase: z.enum(['true', 'false']).optional(),\n isActive: z.enum(['true', 'false']).optional(),\n code: z.string().optional(),\n}).loose()\n\ntype CurrencyRow = {\n id: string\n code: string\n name: string\n symbol: string | null\n decimalPlaces: number\n thousandsSeparator: string | null\n decimalSeparator: string | null\n isBase: boolean\n isActive: boolean\n createdAt: string | null\n updatedAt: string | null\n organizationId: string\n tenantId: string\n}\n\nconst toRow = (currency: Currency): CurrencyRow => ({\n id: String(currency.id),\n code: String(currency.code),\n name: String(currency.name),\n symbol: currency.symbol ?? null,\n decimalPlaces: currency.decimalPlaces,\n thousandsSeparator: currency.thousandsSeparator ?? null,\n decimalSeparator: currency.decimalSeparator ?? null,\n isBase: !!currency.isBase,\n isActive: !!currency.isActive,\n createdAt: currency.createdAt ? currency.createdAt.toISOString() : null,\n updatedAt: currency.updatedAt ? currency.updatedAt.toISOString() : null,\n organizationId: String(currency.organizationId),\n tenantId: String(currency.tenantId),\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = listQuerySchema.safeParse({\n id: url.searchParams.get('id') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sortField: url.searchParams.get('sortField') ?? undefined,\n sortDir: url.searchParams.get('sortDir') ?? undefined,\n isBase: url.searchParams.get('isBase') ?? undefined,\n isActive: url.searchParams.get('isActive') ?? undefined,\n code: url.searchParams.get('code') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { id, page, pageSize, search, sortField, sortDir, isBase, isActive, code } = parsed.data\n const filter: FilterQuery<Currency> = {\n tenantId: auth.tenantId,\n deletedAt: null,\n }\n if (auth.orgId) {\n filter.organizationId = auth.orgId\n }\n \n if (id) filter.id = id\n if (code) filter.code = code\n if (search) {\n filter.$or = [\n { code: { $ilike: `%${search}%` } },\n { name: { $ilike: `%${search}%` } },\n { symbol: { $ilike: `%${search}%` } },\n ]\n }\n if (isBase === 'true') filter.isBase = true\n if (isBase === 'false') filter.isBase = false\n if (isActive === 'true') filter.isActive = true\n if (isActive === 'false') filter.isActive = false\n\n const fieldMap: Record<string, string> = {\n code: 'code',\n name: 'name',\n createdAt: 'createdAt',\n updatedAt: 'updatedAt',\n }\n const orderBy: Record<string, 'ASC' | 'DESC'> = {}\n if (sortField) {\n const mapped = fieldMap[sortField] || 'code'\n orderBy[mapped] = sortDir === 'desc' ? 'DESC' : 'ASC'\n } else {\n orderBy.code = 'ASC'\n }\n\n const offset = (page - 1) * pageSize\n const [rows, total] = await em.findAndCount(Currency, filter, { orderBy, limit: pageSize, offset })\n const items = rows.map(toRow)\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n return NextResponse.json({ items, total, page, pageSize, totalPages })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst currencyListItemSchema = z.object({\n id: z.uuid(),\n code: z.string(),\n name: z.string(),\n symbol: z.string().nullable(),\n decimalPlaces: z.number(),\n thousandsSeparator: z.string().nullable(),\n decimalSeparator: z.string().nullable(),\n isBase: z.boolean(),\n isActive: z.boolean(),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n organizationId: z.uuid(),\n tenantId: z.uuid(),\n})\n\nexport const openApi = createCurrenciesCrudOpenApi({\n resourceName: 'Currency',\n pluralName: 'Currencies',\n querySchema: listQuerySchema,\n listResponseSchema: createPagedListResponseSchema(currencyListItemSchema),\n create: {\n schema: currencyCreateSchema,\n description: 'Creates a new currency.',\n },\n update: {\n schema: currencyUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates an existing currency by id.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a currency by id.',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { Currency } from '../../data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { currencyCreateSchema, currencyUpdateSchema } from '../../data/validators'\nimport {\n createCurrenciesCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['currencies.view'] },\n POST: { requireAuth: true, requireFeatures: ['currencies.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['currencies.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['currencies.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).loose()\ntype CrudInput = Record<string, unknown>\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: Currency,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n events: {\n module: 'currencies',\n entity: 'currency',\n persistent: true,\n },\n actions: {\n create: {\n commandId: 'currencies.currencies.create',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: ({ result }) => ({ id: String(result.currencyId) }),\n status: 201,\n },\n update: {\n commandId: 'currencies.currencies.update',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'currencies.currencies.delete',\n schema: rawBodySchema,\n mapInput: ({ raw, ctx }) => ({\n id: ((raw as Record<string, unknown>).query as Record<string, unknown> | undefined)?.id as string | undefined,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? undefined,\n tenantId: ctx.auth?.tenantId ?? undefined,\n }),\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst listQuerySchema = z.object({\n id: z.uuid().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n sortField: z.enum(['code', 'name', 'createdAt', 'updatedAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n isBase: z.enum(['true', 'false']).optional(),\n isActive: z.enum(['true', 'false']).optional(),\n code: z.string().optional(),\n}).loose()\n\ntype CurrencyRow = {\n id: string\n code: string\n name: string\n symbol: string | null\n decimalPlaces: number\n thousandsSeparator: string | null\n decimalSeparator: string | null\n isBase: boolean\n isActive: boolean\n createdAt: string | null\n updatedAt: string | null\n organizationId: string\n tenantId: string\n}\n\nconst toRow = (currency: Currency): CurrencyRow => ({\n id: String(currency.id),\n code: String(currency.code),\n name: String(currency.name),\n symbol: currency.symbol ?? null,\n decimalPlaces: currency.decimalPlaces,\n thousandsSeparator: currency.thousandsSeparator ?? null,\n decimalSeparator: currency.decimalSeparator ?? null,\n isBase: !!currency.isBase,\n isActive: !!currency.isActive,\n createdAt: currency.createdAt ? currency.createdAt.toISOString() : null,\n updatedAt: currency.updatedAt ? currency.updatedAt.toISOString() : null,\n organizationId: String(currency.organizationId),\n tenantId: String(currency.tenantId),\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = listQuerySchema.safeParse({\n id: url.searchParams.get('id') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sortField: url.searchParams.get('sortField') ?? undefined,\n sortDir: url.searchParams.get('sortDir') ?? undefined,\n isBase: url.searchParams.get('isBase') ?? undefined,\n isActive: url.searchParams.get('isActive') ?? undefined,\n code: url.searchParams.get('code') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { id, page, pageSize, search, sortField, sortDir, isBase, isActive, code } = parsed.data\n const filter: FilterQuery<Currency> = {\n tenantId: auth.tenantId,\n deletedAt: null,\n }\n if (auth.orgId) {\n filter.organizationId = auth.orgId\n }\n \n if (id) filter.id = id\n if (code) filter.code = code\n if (search) {\n filter.$or = [\n { code: { $ilike: `%${escapeLikePattern(search)}%` } },\n { name: { $ilike: `%${escapeLikePattern(search)}%` } },\n { symbol: { $ilike: `%${escapeLikePattern(search)}%` } },\n ]\n }\n if (isBase === 'true') filter.isBase = true\n if (isBase === 'false') filter.isBase = false\n if (isActive === 'true') filter.isActive = true\n if (isActive === 'false') filter.isActive = false\n\n const fieldMap: Record<string, string> = {\n code: 'code',\n name: 'name',\n createdAt: 'createdAt',\n updatedAt: 'updatedAt',\n }\n const orderBy: Record<string, 'ASC' | 'DESC'> = {}\n if (sortField) {\n const mapped = fieldMap[sortField] || 'code'\n orderBy[mapped] = sortDir === 'desc' ? 'DESC' : 'ASC'\n } else {\n orderBy.code = 'ASC'\n }\n\n const offset = (page - 1) * pageSize\n const [rows, total] = await em.findAndCount(Currency, filter, { orderBy, limit: pageSize, offset })\n const items = rows.map(toRow)\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n return NextResponse.json({ items, total, page, pageSize, totalPages })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst currencyListItemSchema = z.object({\n id: z.uuid(),\n code: z.string(),\n name: z.string(),\n symbol: z.string().nullable(),\n decimalPlaces: z.number(),\n thousandsSeparator: z.string().nullable(),\n decimalSeparator: z.string().nullable(),\n isBase: z.boolean(),\n isActive: z.boolean(),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n organizationId: z.uuid(),\n tenantId: z.uuid(),\n})\n\nexport const openApi = createCurrenciesCrudOpenApi({\n resourceName: 'Currency',\n pluralName: 'Currencies',\n querySchema: listQuerySchema,\n listResponseSchema: createPagedListResponseSchema(currencyListItemSchema),\n create: {\n schema: currencyCreateSchema,\n description: 'Creates a new currency.',\n },\n update: {\n schema: currencyUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates an existing currency by id.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a currency by id.',\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AAGzB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,yBAAyB;AAClC,SAAS,sBAAsB,4BAA4B;AAC3D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,iBAAiB,EAAE;AAAA,EAC/D,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AAAA,EAClE,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AAAA,EACjE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACtE;AAEO,MAAM,WAAW;AAExB,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,MAAM;AAGzC,MAAM,OAAO,cAA6D;AAAA,EACxE,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,YAAY;AAAA,EACd;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,CAAC,EAAE,OAAO,MAAM;AAAA,MAC1B,UAAU,CAAC,EAAE,OAAO,OAAO,EAAE,IAAI,OAAO,OAAO,UAAU,EAAE;AAAA,MAC3D,QAAQ;AAAA,IACV;AAAA,IACA,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,CAAC,EAAE,OAAO,MAAM;AAAA,MAC1B,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,IACA,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,CAAC,EAAE,KAAK,IAAI,OAAO;AAAA,QAC3B,IAAM,IAAgC,OAA+C;AAAA,QACrF,gBAAgB,IAAI,0BAA0B,IAAI,MAAM,SAAS;AAAA,QACjE,UAAU,IAAI,MAAM,YAAY;AAAA,MAClC;AAAA,MACA,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,EACF;AACF,CAAC;AAED,MAAM,kBAAkB,EAAE,OAAO;AAAA,EAC/B,IAAI,EAAE,KAAK,EAAE,SAAS;AAAA,EACtB,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,WAAW,EAAE,KAAK,CAAC,QAAQ,QAAQ,aAAa,WAAW,CAAC,EAAE,SAAS;AAAA,EACvE,SAAS,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAAA,EAC1C,QAAQ,EAAE,KAAK,CAAC,QAAQ,OAAO,CAAC,EAAE,SAAS;AAAA,EAC3C,UAAU,EAAE,KAAK,CAAC,QAAQ,OAAO,CAAC,EAAE,SAAS;AAAA,EAC7C,MAAM,EAAE,OAAO,EAAE,SAAS;AAC5B,CAAC,EAAE,MAAM;AAkBT,MAAM,QAAQ,CAAC,cAAqC;AAAA,EAClD,IAAI,OAAO,SAAS,EAAE;AAAA,EACtB,MAAM,OAAO,SAAS,IAAI;AAAA,EAC1B,MAAM,OAAO,SAAS,IAAI;AAAA,EAC1B,QAAQ,SAAS,UAAU;AAAA,EAC3B,eAAe,SAAS;AAAA,EACxB,oBAAoB,SAAS,sBAAsB;AAAA,EACnD,kBAAkB,SAAS,oBAAoB;AAAA,EAC/C,QAAQ,CAAC,CAAC,SAAS;AAAA,EACnB,UAAU,CAAC,CAAC,SAAS;AAAA,EACrB,WAAW,SAAS,YAAY,SAAS,UAAU,YAAY,IAAI;AAAA,EACnE,WAAW,SAAS,YAAY,SAAS,UAAU,YAAY,IAAI;AAAA,EACnE,gBAAgB,OAAO,SAAS,cAAc;AAAA,EAC9C,UAAU,OAAO,SAAS,QAAQ;AACpC;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,YAAa,CAAC,KAAK,SAAS,CAAC,KAAK,cAAe;AAClE,WAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,IAAI,YAAY,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzG;AAEA,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,SAAS,gBAAgB,UAAU;AAAA,IACvC,IAAI,IAAI,aAAa,IAAI,IAAI,KAAK;AAAA,IAClC,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,IACtC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,IAC9C,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,IAC1C,WAAW,IAAI,aAAa,IAAI,WAAW,KAAK;AAAA,IAChD,SAAS,IAAI,aAAa,IAAI,SAAS,KAAK;AAAA,IAC5C,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,IAC1C,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,IAC9C,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,EACxC,CAAC;AACD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,IAAI,YAAY,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzG;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,QAAM,EAAE,IAAI,MAAM,UAAU,QAAQ,WAAW,SAAS,QAAQ,UAAU,KAAK,IAAI,OAAO;AAC1F,QAAM,SAAgC;AAAA,IACpC,UAAU,KAAK;AAAA,IACf,WAAW;AAAA,EACb;AACA,MAAI,KAAK,OAAO;AACd,WAAO,iBAAiB,KAAK;AAAA,EAC/B;AAEA,MAAI,GAAI,QAAO,KAAK;AACpB,MAAI,KAAM,QAAO,OAAO;AACxB,MAAI,QAAQ;AACV,WAAO,MAAM;AAAA,MACX,EAAE,MAAM,EAAE,QAAQ,IAAI,kBAAkB,MAAM,CAAC,IAAI,EAAE;AAAA,MACrD,EAAE,MAAM,EAAE,QAAQ,IAAI,kBAAkB,MAAM,CAAC,IAAI,EAAE;AAAA,MACrD,EAAE,QAAQ,EAAE,QAAQ,IAAI,kBAAkB,MAAM,CAAC,IAAI,EAAE;AAAA,IACzD;AAAA,EACF;AACA,MAAI,WAAW,OAAQ,QAAO,SAAS;AACvC,MAAI,WAAW,QAAS,QAAO,SAAS;AACxC,MAAI,aAAa,OAAQ,QAAO,WAAW;AAC3C,MAAI,aAAa,QAAS,QAAO,WAAW;AAE5C,QAAM,WAAmC;AAAA,IACvC,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACA,QAAM,UAA0C,CAAC;AACjD,MAAI,WAAW;AACb,UAAM,SAAS,SAAS,SAAS,KAAK;AACtC,YAAQ,MAAM,IAAI,YAAY,SAAS,SAAS;AAAA,EAClD,OAAO;AACL,YAAQ,OAAO;AAAA,EACjB;AAEA,QAAM,UAAU,OAAO,KAAK;AAC5B,QAAM,CAAC,MAAM,KAAK,IAAI,MAAM,GAAG,aAAa,UAAU,QAAQ,EAAE,SAAS,OAAO,UAAU,OAAO,CAAC;AAClG,QAAM,QAAQ,KAAK,IAAI,KAAK;AAC5B,QAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,QAAQ,CAAC;AAE1D,SAAO,aAAa,KAAK,EAAE,OAAO,OAAO,MAAM,UAAU,WAAW,CAAC;AACvE;AAEO,MAAM,OAAO,KAAK;AAClB,MAAM,MAAM,KAAK;AACjB,MAAM,SAAS,KAAK;AAE3B,MAAM,yBAAyB,EAAE,OAAO;AAAA,EACtC,IAAI,EAAE,KAAK;AAAA,EACX,MAAM,EAAE,OAAO;AAAA,EACf,MAAM,EAAE,OAAO;AAAA,EACf,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,eAAe,EAAE,OAAO;AAAA,EACxB,oBAAoB,EAAE,OAAO,EAAE,SAAS;AAAA,EACxC,kBAAkB,EAAE,OAAO,EAAE,SAAS;AAAA,EACtC,QAAQ,EAAE,QAAQ;AAAA,EAClB,UAAU,EAAE,QAAQ;AAAA,EACpB,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,gBAAgB,EAAE,KAAK;AAAA,EACvB,UAAU,EAAE,KAAK;AACnB,CAAC;AAEM,MAAM,UAAU,4BAA4B;AAAA,EACjD,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,oBAAoB,8BAA8B,sBAAsB;AAAA,EACxE,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aAAa;AAAA,EACf;AAAA,EACA,KAAK;AAAA,IACH,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,IAC1C,gBAAgB;AAAA,IAChB,aAAa;AAAA,EACf;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
5
6
  import { CustomerRole, CustomerRoleAcl } from "@open-mercato/core/modules/customer_accounts/data/entities";
6
7
  import { createRoleSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
7
8
  import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
@@ -28,7 +29,7 @@ async function GET(req) {
28
29
  deletedAt: null
29
30
  };
30
31
  if (search) {
31
- const escapedSearch = search.replace(/[%_\\]/g, "\\$&");
32
+ const escapedSearch = escapeLikePattern(search);
32
33
  where.$or = [
33
34
  { name: { $ilike: `%${escapedSearch}%` } },
34
35
  { slug: { $ilike: `%${escapedSearch}%` } }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customer_accounts/api/admin/roles.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { createRoleSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\n\nexport const metadata = {}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.view'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10) || 1)\n const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get('pageSize') || '50', 10) || 50))\n const search = (url.searchParams.get('search') || '').trim()\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const where: Record<string, unknown> = {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n deletedAt: null,\n }\n\n if (search) {\n const escapedSearch = search.replace(/[%_\\\\]/g, '\\\\$&')\n where.$or = [\n { name: { $ilike: `%${escapedSearch}%` } },\n { slug: { $ilike: `%${escapedSearch}%` } },\n ]\n }\n\n const offset = (page - 1) * pageSize\n const [paged, total] = await em.findAndCount(CustomerRole, where as any, {\n orderBy: { createdAt: 'ASC' },\n limit: pageSize,\n offset,\n })\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n const items = paged.map((role) => ({\n id: role.id,\n name: role.name,\n slug: role.slug,\n description: role.description || null,\n isDefault: role.isDefault,\n isSystem: role.isSystem,\n customerAssignable: role.customerAssignable,\n createdAt: role.createdAt,\n updatedAt: role.updatedAt || null,\n }))\n\n return NextResponse.json({ ok: true, items, total, totalPages, page })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.roles.manage'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = createRoleSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const existing = await em.findOne(CustomerRole, {\n tenantId: auth.tenantId,\n slug: parsed.data.slug,\n deletedAt: null,\n })\n if (existing) {\n return NextResponse.json({ ok: false, error: 'A role with this slug already exists' }, { status: 409 })\n }\n\n const role = em.create(CustomerRole, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n name: parsed.data.name,\n slug: parsed.data.slug,\n description: parsed.data.description || null,\n isDefault: parsed.data.isDefault ?? false,\n customerAssignable: parsed.data.customerAssignable ?? false,\n isSystem: false,\n createdAt: new Date(),\n } as any) as CustomerRole\n em.persist(role)\n\n const acl = em.create(CustomerRoleAcl, {\n role,\n tenantId: auth.tenantId,\n featuresJson: [],\n isPortalAdmin: false,\n createdAt: new Date(),\n } as any)\n em.persist(acl)\n\n await em.flush()\n\n void emitCustomerAccountsEvent('customer_accounts.role.created', {\n id: role.id,\n name: role.name,\n slug: role.slug,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }).catch(() => undefined)\n\n return NextResponse.json({\n ok: true,\n role: {\n id: role.id,\n name: role.name,\n slug: role.slug,\n description: role.description || null,\n isDefault: role.isDefault,\n isSystem: role.isSystem,\n customerAssignable: role.customerAssignable,\n createdAt: role.createdAt,\n },\n }, { status: 201 })\n}\n\nconst roleSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n slug: z.string(),\n description: z.string().nullable(),\n isDefault: z.boolean(),\n isSystem: z.boolean(),\n customerAssignable: z.boolean(),\n createdAt: z.string().datetime(),\n updatedAt: z.string().datetime().nullable(),\n})\n\nconst roleResponseSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n slug: z.string(),\n description: z.string().nullable(),\n isDefault: z.boolean(),\n isSystem: z.boolean(),\n customerAssignable: z.boolean(),\n createdAt: z.string().datetime(),\n})\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst getMethodDoc: OpenApiMethodDoc = {\n summary: 'List customer roles (admin)',\n description: 'Returns all customer roles for the tenant.',\n tags: ['Customer Accounts Admin'],\n responses: [{\n status: 200,\n description: 'Role list',\n schema: z.object({ ok: z.literal(true), items: z.array(roleSchema), total: z.number(), totalPages: z.number(), page: z.number() }),\n }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n ],\n}\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Create customer role (admin)',\n description: 'Creates a new customer role with an empty ACL.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: createRoleSchema },\n responses: [{ status: 201, description: 'Role created', schema: z.object({ ok: z.literal(true), role: roleResponseSchema }) }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 409, description: 'Slug already exists', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer role management (admin)',\n methods: {\n GET: getMethodDoc,\n POST: postMethodDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { createRoleSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\n\nexport const metadata = {}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.view'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10) || 1)\n const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get('pageSize') || '50', 10) || 50))\n const search = (url.searchParams.get('search') || '').trim()\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const where: Record<string, unknown> = {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n deletedAt: null,\n }\n\n if (search) {\n const escapedSearch = escapeLikePattern(search)\n where.$or = [\n { name: { $ilike: `%${escapedSearch}%` } },\n { slug: { $ilike: `%${escapedSearch}%` } },\n ]\n }\n\n const offset = (page - 1) * pageSize\n const [paged, total] = await em.findAndCount(CustomerRole, where as any, {\n orderBy: { createdAt: 'ASC' },\n limit: pageSize,\n offset,\n })\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n const items = paged.map((role) => ({\n id: role.id,\n name: role.name,\n slug: role.slug,\n description: role.description || null,\n isDefault: role.isDefault,\n isSystem: role.isSystem,\n customerAssignable: role.customerAssignable,\n createdAt: role.createdAt,\n updatedAt: role.updatedAt || null,\n }))\n\n return NextResponse.json({ ok: true, items, total, totalPages, page })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.roles.manage'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = createRoleSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const existing = await em.findOne(CustomerRole, {\n tenantId: auth.tenantId,\n slug: parsed.data.slug,\n deletedAt: null,\n })\n if (existing) {\n return NextResponse.json({ ok: false, error: 'A role with this slug already exists' }, { status: 409 })\n }\n\n const role = em.create(CustomerRole, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n name: parsed.data.name,\n slug: parsed.data.slug,\n description: parsed.data.description || null,\n isDefault: parsed.data.isDefault ?? false,\n customerAssignable: parsed.data.customerAssignable ?? false,\n isSystem: false,\n createdAt: new Date(),\n } as any) as CustomerRole\n em.persist(role)\n\n const acl = em.create(CustomerRoleAcl, {\n role,\n tenantId: auth.tenantId,\n featuresJson: [],\n isPortalAdmin: false,\n createdAt: new Date(),\n } as any)\n em.persist(acl)\n\n await em.flush()\n\n void emitCustomerAccountsEvent('customer_accounts.role.created', {\n id: role.id,\n name: role.name,\n slug: role.slug,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }).catch(() => undefined)\n\n return NextResponse.json({\n ok: true,\n role: {\n id: role.id,\n name: role.name,\n slug: role.slug,\n description: role.description || null,\n isDefault: role.isDefault,\n isSystem: role.isSystem,\n customerAssignable: role.customerAssignable,\n createdAt: role.createdAt,\n },\n }, { status: 201 })\n}\n\nconst roleSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n slug: z.string(),\n description: z.string().nullable(),\n isDefault: z.boolean(),\n isSystem: z.boolean(),\n customerAssignable: z.boolean(),\n createdAt: z.string().datetime(),\n updatedAt: z.string().datetime().nullable(),\n})\n\nconst roleResponseSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n slug: z.string(),\n description: z.string().nullable(),\n isDefault: z.boolean(),\n isSystem: z.boolean(),\n customerAssignable: z.boolean(),\n createdAt: z.string().datetime(),\n})\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst getMethodDoc: OpenApiMethodDoc = {\n summary: 'List customer roles (admin)',\n description: 'Returns all customer roles for the tenant.',\n tags: ['Customer Accounts Admin'],\n responses: [{\n status: 200,\n description: 'Role list',\n schema: z.object({ ok: z.literal(true), items: z.array(roleSchema), total: z.number(), totalPages: z.number(), page: z.number() }),\n }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n ],\n}\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Create customer role (admin)',\n description: 'Creates a new customer role with an empty ACL.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: createRoleSchema },\n responses: [{ status: 201, description: 'Role created', schema: z.object({ ok: z.literal(true), role: roleResponseSchema }) }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 409, description: 'Slug already exists', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer role management (admin)',\n methods: {\n GET: getMethodDoc,\n POST: postMethodDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,13 +1,11 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { AlertTriangle } from "lucide-react";
4
3
  import { Alert, AlertTitle, AlertDescription } from "@open-mercato/ui/primitives/alert";
5
4
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
5
  function DnsDiagnostics({ mapping }) {
7
6
  const t = useT();
8
7
  if (mapping.status !== "dns_failed") return null;
9
8
  return /* @__PURE__ */ jsxs(Alert, { variant: "destructive", children: [
10
- /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4", "aria-hidden": true }),
11
9
  /* @__PURE__ */ jsx(AlertTitle, { children: t("customer_accounts.domainMapping.dns.diagnostics.title", "DNS configuration issue") }),
12
10
  /* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
13
11
  mapping.dnsFailureReason ? /* @__PURE__ */ jsx("p", { children: mapping.dnsFailureReason }) : null,
@@ -22,7 +20,6 @@ function TlsDiagnostics({ mapping }) {
22
20
  const t = useT();
23
21
  if (mapping.status !== "tls_failed") return null;
24
22
  return /* @__PURE__ */ jsxs(Alert, { variant: "warning", children: [
25
- /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4", "aria-hidden": true }),
26
23
  /* @__PURE__ */ jsx(AlertTitle, { children: t("customer_accounts.domainMapping.tls.diagnostics.title", "SSL certificate issue") }),
27
24
  /* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
28
25
  mapping.tlsFailureReason ? /* @__PURE__ */ jsx("p", { children: mapping.tlsFailureReason }) : null,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { AlertTriangle } from 'lucide-react'\nimport { Alert, AlertTitle, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { DomainMappingRow } from './types'\n\nexport type DiagnosticsProps = {\n mapping: DomainMappingRow\n}\n\nexport function DnsDiagnostics({ mapping }: DiagnosticsProps) {\n const t = useT()\n if (mapping.status !== 'dns_failed') return null\n return (\n <Alert variant=\"destructive\">\n <AlertTriangle className=\"h-4 w-4\" aria-hidden />\n <AlertTitle>\n {t('customer_accounts.domainMapping.dns.diagnostics.title', 'DNS configuration issue')}\n </AlertTitle>\n <AlertDescription>\n <div className=\"space-y-2\">\n {mapping.dnsFailureReason ? <p>{mapping.dnsFailureReason}</p> : null}\n {mapping.cnameTarget ? (\n <dl className=\"grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-xs\">\n <dt className=\"text-muted-foreground\">\n {t('customer_accounts.domainMapping.dns.diagnostics.expected', 'Expected target')}\n </dt>\n <dd className=\"font-mono\">{mapping.cnameTarget}</dd>\n </dl>\n ) : null}\n </div>\n </AlertDescription>\n </Alert>\n )\n}\n\nexport function TlsDiagnostics({ mapping }: DiagnosticsProps) {\n const t = useT()\n if (mapping.status !== 'tls_failed') return null\n return (\n <Alert variant=\"warning\">\n <AlertTriangle className=\"h-4 w-4\" aria-hidden />\n <AlertTitle>\n {t('customer_accounts.domainMapping.tls.diagnostics.title', 'SSL certificate issue')}\n </AlertTitle>\n <AlertDescription>\n <div className=\"space-y-2\">\n {mapping.tlsFailureReason ? <p>{mapping.tlsFailureReason}</p> : null}\n <p className=\"text-xs text-muted-foreground\">\n {t('customer_accounts.domainMapping.tls.diagnostics.retryCount', 'Retry attempts: {count}', {\n count: String(mapping.tlsRetryCount),\n })}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'customer_accounts.domainMapping.tls.diagnostics.operatorNote',\n 'We are retrying automatically. If this persists, contact platform support \u2014 your DNS is fine, this is on our side.',\n )}\n </p>\n </div>\n </AlertDescription>\n </Alert>\n )\n}\n"],
5
- "mappings": ";AAiBM,cAQM,YARN;AAdN,SAAS,qBAAqB;AAC9B,SAAS,OAAO,YAAY,wBAAwB;AACpD,SAAS,YAAY;AAOd,SAAS,eAAe,EAAE,QAAQ,GAAqB;AAC5D,QAAM,IAAI,KAAK;AACf,MAAI,QAAQ,WAAW,aAAc,QAAO;AAC5C,SACE,qBAAC,SAAM,SAAQ,eACb;AAAA,wBAAC,iBAAc,WAAU,WAAU,eAAW,MAAC;AAAA,IAC/C,oBAAC,cACE,YAAE,yDAAyD,yBAAyB,GACvF;AAAA,IACA,oBAAC,oBACC,+BAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,mBAAmB,oBAAC,OAAG,kBAAQ,kBAAiB,IAAO;AAAA,MAC/D,QAAQ,cACP,qBAAC,QAAG,WAAU,4DACZ;AAAA,4BAAC,QAAG,WAAU,yBACX,YAAE,4DAA4D,iBAAiB,GAClF;AAAA,QACA,oBAAC,QAAG,WAAU,aAAa,kBAAQ,aAAY;AAAA,SACjD,IACE;AAAA,OACN,GACF;AAAA,KACF;AAEJ;AAEO,SAAS,eAAe,EAAE,QAAQ,GAAqB;AAC5D,QAAM,IAAI,KAAK;AACf,MAAI,QAAQ,WAAW,aAAc,QAAO;AAC5C,SACE,qBAAC,SAAM,SAAQ,WACb;AAAA,wBAAC,iBAAc,WAAU,WAAU,eAAW,MAAC;AAAA,IAC/C,oBAAC,cACE,YAAE,yDAAyD,uBAAuB,GACrF;AAAA,IACA,oBAAC,oBACC,+BAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,mBAAmB,oBAAC,OAAG,kBAAQ,kBAAiB,IAAO;AAAA,MAChE,oBAAC,OAAE,WAAU,iCACV,YAAE,8DAA8D,2BAA2B;AAAA,QAC1F,OAAO,OAAO,QAAQ,aAAa;AAAA,MACrC,CAAC,GACH;AAAA,MACA,oBAAC,OAAE,WAAU,iCACV;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF;AAAA,OACF,GACF;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Alert, AlertTitle, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { DomainMappingRow } from './types'\n\nexport type DiagnosticsProps = {\n mapping: DomainMappingRow\n}\n\nexport function DnsDiagnostics({ mapping }: DiagnosticsProps) {\n const t = useT()\n if (mapping.status !== 'dns_failed') return null\n return (\n <Alert variant=\"destructive\">\n <AlertTitle>\n {t('customer_accounts.domainMapping.dns.diagnostics.title', 'DNS configuration issue')}\n </AlertTitle>\n <AlertDescription>\n <div className=\"space-y-2\">\n {mapping.dnsFailureReason ? <p>{mapping.dnsFailureReason}</p> : null}\n {mapping.cnameTarget ? (\n <dl className=\"grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-xs\">\n <dt className=\"text-muted-foreground\">\n {t('customer_accounts.domainMapping.dns.diagnostics.expected', 'Expected target')}\n </dt>\n <dd className=\"font-mono\">{mapping.cnameTarget}</dd>\n </dl>\n ) : null}\n </div>\n </AlertDescription>\n </Alert>\n )\n}\n\nexport function TlsDiagnostics({ mapping }: DiagnosticsProps) {\n const t = useT()\n if (mapping.status !== 'tls_failed') return null\n return (\n <Alert variant=\"warning\">\n <AlertTitle>\n {t('customer_accounts.domainMapping.tls.diagnostics.title', 'SSL certificate issue')}\n </AlertTitle>\n <AlertDescription>\n <div className=\"space-y-2\">\n {mapping.tlsFailureReason ? <p>{mapping.tlsFailureReason}</p> : null}\n <p className=\"text-xs text-muted-foreground\">\n {t('customer_accounts.domainMapping.tls.diagnostics.retryCount', 'Retry attempts: {count}', {\n count: String(mapping.tlsRetryCount),\n })}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'customer_accounts.domainMapping.tls.diagnostics.operatorNote',\n 'We are retrying automatically. If this persists, contact platform support \u2014 your DNS is fine, this is on our side.',\n )}\n </p>\n </div>\n </AlertDescription>\n </Alert>\n )\n}\n"],
5
+ "mappings": ";AAgBM,cAOM,YAPN;AAbN,SAAS,OAAO,YAAY,wBAAwB;AACpD,SAAS,YAAY;AAOd,SAAS,eAAe,EAAE,QAAQ,GAAqB;AAC5D,QAAM,IAAI,KAAK;AACf,MAAI,QAAQ,WAAW,aAAc,QAAO;AAC5C,SACE,qBAAC,SAAM,SAAQ,eACb;AAAA,wBAAC,cACE,YAAE,yDAAyD,yBAAyB,GACvF;AAAA,IACA,oBAAC,oBACC,+BAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,mBAAmB,oBAAC,OAAG,kBAAQ,kBAAiB,IAAO;AAAA,MAC/D,QAAQ,cACP,qBAAC,QAAG,WAAU,4DACZ;AAAA,4BAAC,QAAG,WAAU,yBACX,YAAE,4DAA4D,iBAAiB,GAClF;AAAA,QACA,oBAAC,QAAG,WAAU,aAAa,kBAAQ,aAAY;AAAA,SACjD,IACE;AAAA,OACN,GACF;AAAA,KACF;AAEJ;AAEO,SAAS,eAAe,EAAE,QAAQ,GAAqB;AAC5D,QAAM,IAAI,KAAK;AACf,MAAI,QAAQ,WAAW,aAAc,QAAO;AAC5C,SACE,qBAAC,SAAM,SAAQ,WACb;AAAA,wBAAC,cACE,YAAE,yDAAyD,uBAAuB,GACrF;AAAA,IACA,oBAAC,oBACC,+BAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,mBAAmB,oBAAC,OAAG,kBAAQ,kBAAiB,IAAO;AAAA,MAChE,oBAAC,OAAE,WAAU,iCACV,YAAE,8DAA8D,2BAA2B;AAAA,QAC1F,OAAO,OAAO,QAAQ,aAAa;AAAA,MACrC,CAAC,GACH;AAAA,MACA,oBAAC,OAAE,WAAU,iCACV;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF;AAAA,OACF,GACF;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -17,7 +17,7 @@ const events = [
17
17
  { id: "customer_accounts.role.deleted", label: "Customer Role Deleted", entity: "role", category: "crud" },
18
18
  { id: "customer_accounts.invitation.accepted", label: "Customer Invitation Accepted", category: "lifecycle", clientBroadcast: true },
19
19
  { id: "customer_accounts.password_reset.requested", label: "Customer Password Reset Requested", category: "lifecycle" },
20
- // Custom domain mapping lifecycle (see .ai/specs/2026-04-08-portal-custom-domain-routing.md)
20
+ // Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)
21
21
  { id: "customer_accounts.domain_mapping.created", label: "Custom Domain Registered", entity: "domain_mapping", category: "crud", clientBroadcast: true },
22
22
  { id: "customer_accounts.domain_mapping.verified", label: "Custom Domain DNS Verified", entity: "domain_mapping", category: "lifecycle", clientBroadcast: true },
23
23
  { id: "customer_accounts.domain_mapping.activated", label: "Custom Domain Active", entity: "domain_mapping", category: "lifecycle", clientBroadcast: true },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/customer_accounts/events.ts"],
4
- "sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'customer_accounts.user.created', label: 'Customer User Created', entity: 'user', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.user.updated', label: 'Customer User Updated', entity: 'user', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.user.deleted', label: 'Customer User Deleted', entity: 'user', category: 'crud' },\n { id: 'customer_accounts.user.locked', label: 'Customer User Locked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.user.unlocked', label: 'Customer User Unlocked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.login.success', label: 'Customer Login Successful', category: 'lifecycle' },\n { id: 'customer_accounts.login.failed', label: 'Customer Login Failed', category: 'lifecycle' },\n { id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },\n { id: 'customer_accounts.email.verified', label: 'Customer Email Verified', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.reset_requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n { id: 'customer_accounts.password.reset', label: 'Customer Password Reset', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.changed', label: 'Customer Password Changed', category: 'lifecycle' },\n { id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n // Custom domain mapping lifecycle (see .ai/specs/2026-04-08-portal-custom-domain-routing.md)\n { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.dns_failed', label: 'Custom Domain DNS Verification Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.tls_failed', label: 'Custom Domain SSL Provisioning Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.deleted', label: 'Custom Domain Removed', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.replaced', label: 'Custom Domain Replaced', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'customer_accounts',\n events,\n})\n\nexport const emitCustomerAccountsEvent = eventsConfig.emit\n\nexport type CustomerAccountsEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
4
+ "sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'customer_accounts.user.created', label: 'Customer User Created', entity: 'user', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.user.updated', label: 'Customer User Updated', entity: 'user', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.user.deleted', label: 'Customer User Deleted', entity: 'user', category: 'crud' },\n { id: 'customer_accounts.user.locked', label: 'Customer User Locked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.user.unlocked', label: 'Customer User Unlocked', entity: 'user', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.login.success', label: 'Customer Login Successful', category: 'lifecycle' },\n { id: 'customer_accounts.login.failed', label: 'Customer Login Failed', category: 'lifecycle' },\n { id: 'customer_accounts.magic_link.requested', label: 'Customer Magic Link Requested', category: 'lifecycle' },\n { id: 'customer_accounts.email.verified', label: 'Customer Email Verified', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.reset_requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n { id: 'customer_accounts.password.reset', label: 'Customer Password Reset', category: 'lifecycle', portalBroadcast: true },\n { id: 'customer_accounts.password.changed', label: 'Customer Password Changed', category: 'lifecycle' },\n { id: 'customer_accounts.role.created', label: 'Customer Role Created', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.role.updated', label: 'Customer Role Updated', entity: 'role', category: 'crud', portalBroadcast: true },\n { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },\n { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },\n // Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)\n { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.dns_failed', label: 'Custom Domain DNS Verification Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.tls_failed', label: 'Custom Domain SSL Provisioning Failed', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.deleted', label: 'Custom Domain Removed', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },\n { id: 'customer_accounts.domain_mapping.replaced', label: 'Custom Domain Replaced', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'customer_accounts',\n events,\n})\n\nexport const emitCustomerAccountsEvent = eventsConfig.emit\n\nexport type CustomerAccountsEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
5
5
  "mappings": "AAAA,SAAS,0BAA0B;AAEnC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,iCAAiC,OAAO,wBAAwB,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,mCAAmC,OAAO,0BAA0B,QAAQ,QAAQ,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACvI,EAAE,IAAI,mCAAmC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACnG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,UAAU,YAAY;AAAA,EAC9F,EAAE,IAAI,0CAA0C,OAAO,iCAAiC,UAAU,YAAY;AAAA,EAC9G,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA,EACtH,EAAE,IAAI,oCAAoC,OAAO,2BAA2B,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACzH,EAAE,IAAI,sCAAsC,OAAO,6BAA6B,UAAU,YAAY;AAAA,EACtG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAChI,EAAE,IAAI,kCAAkC,OAAO,yBAAyB,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACzG,EAAE,IAAI,yCAAyC,OAAO,gCAAgC,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACnI,EAAE,IAAI,8CAA8C,OAAO,qCAAqC,UAAU,YAAY;AAAA;AAAA,EAEtH,EAAE,IAAI,4CAA4C,OAAO,4BAA4B,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACvJ,EAAE,IAAI,6CAA6C,OAAO,8BAA8B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC/J,EAAE,IAAI,8CAA8C,OAAO,wBAAwB,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC1J,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,+CAA+C,OAAO,yCAAyC,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAC5K,EAAE,IAAI,4CAA4C,OAAO,yBAAyB,QAAQ,kBAAkB,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EACpJ,EAAE,IAAI,6CAA6C,OAAO,0BAA0B,QAAQ,kBAAkB,UAAU,aAAa,iBAAiB,KAAK;AAC7J;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAEM,MAAM,4BAA4B,aAAa;AAItD,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/lib/resolveTenantContext.ts"],
4
- "sourcesContent": ["/**\n * Resolve the tenant context for a customer-portal request.\n *\n * - Platform-domain hosts: resolve the tenant server-side from the body\n * `organizationId` (org \u2192 tenant). A legacy body `tenantId` is still accepted\n * and cross-checked against the resolved tenant (fail closed on mismatch); if\n * no `organizationId` is supplied it falls back to the legacy `tenantId`.\n * - Custom-domain hosts: resolve via `domainMappingService.resolveByHostname()`.\n * If the body supplied a different `tenantId`, fail closed (mismatch).\n *\n * This is the single entry point used by all customer-auth routes (login,\n * signup, magic-link, password-reset) so they all behave consistently when\n * the request arrives on a tenant's branded URL.\n *\n * See `.ai/specs/2026-04-08-portal-custom-domain-routing.md` Phase 1.5 and\n * `.ai/specs/2026-06-05-tenant-ownership-and-module-acl-authorization.md` \u00A7 C.\n */\n\nimport { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\nexport class TenantResolutionError extends Error {\n constructor(message: string, public readonly status: number) {\n super(message)\n this.name = 'TenantResolutionError'\n }\n}\n\nexport type ResolvedTenantContext = {\n source: 'body' | 'host'\n tenantId: string\n organizationId: string | null\n hostname: string | null\n}\n\nfunction readForcedHost(req: Request): string | null {\n // Test-only override. The middleware honors `X-Force-Host` only when\n // `NODE_ENV === 'test'` AND `X-Force-Host-Secret` matches; we mirror the\n // same check here so request-scoped helpers behave the same way under tests.\n if (process.env.NODE_ENV !== 'test') return null\n const expected = process.env.FORCE_HOST_SECRET\n if (!expected) return null\n if (!secretEqual(req.headers.get('x-force-host-secret'), expected)) return null\n const host = req.headers.get('x-force-host')\n return host && host.length > 0 ? host : null\n}\n\nasync function resolveTenantFromOrganization(\n organizationId: string,\n container: AppContainer | undefined,\n): Promise<{ tenantId: string; organizationId: string }> {\n let em: EntityManager\n if (container) {\n em = container.resolve('em') as EntityManager\n } else {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const requestContainer = await createRequestContainer()\n em = requestContainer.resolve('em') as EntityManager\n }\n\n const organization = await em.findOne(\n Organization,\n { id: organizationId, deletedAt: null },\n { populate: ['tenant'] },\n )\n if (!organization) {\n throw new TenantResolutionError('Organization not found', 400)\n }\n const tenantId = typeof organization.tenant === 'string'\n ? organization.tenant\n : organization.tenant?.id\n ? String(organization.tenant.id)\n : null\n if (!tenantId) {\n throw new TenantResolutionError('Organization not found', 400)\n }\n return { tenantId, organizationId: String(organization.id) }\n}\n\nexport async function resolveTenantContext(\n req: Request,\n bodyTenantId: string | null | undefined,\n options?: { container?: AppContainer; organizationId?: string | null },\n): Promise<ResolvedTenantContext> {\n const rawHost = readForcedHost(req) ?? req.headers.get('host')\n const hostname = rawHost ? tryNormalizeHostname(rawHost) : null\n const isPlatform = hostname ? platformDomains().includes(hostname) : true\n const bodyOrganizationId = options?.organizationId ?? null\n\n if (isPlatform) {\n if (bodyOrganizationId) {\n const resolved = await resolveTenantFromOrganization(bodyOrganizationId, options?.container)\n if (bodyTenantId && bodyTenantId !== resolved.tenantId) {\n throw new TenantResolutionError(\n 'tenantId in request body does not match the resolved organization',\n 400,\n )\n }\n return {\n source: 'body',\n tenantId: resolved.tenantId,\n organizationId: resolved.organizationId,\n hostname,\n }\n }\n if (!bodyTenantId) {\n throw new TenantResolutionError(\n 'organizationId or tenantId is required for platform-domain logins',\n 400,\n )\n }\n return { source: 'body', tenantId: bodyTenantId, organizationId: null, hostname }\n }\n\n // Custom-domain host\n let service: DomainMappingService | null = null\n try {\n if (options?.container) {\n service = options.container.resolve('domainMappingService') as DomainMappingService\n } else {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n service = container.resolve('domainMappingService') as DomainMappingService\n }\n } catch {\n throw new TenantResolutionError('Custom domain routing is not available on this deployment', 503)\n }\n\n if (!hostname) throw new TenantResolutionError('Unable to determine request hostname', 400)\n const resolved = await service.resolveByHostname(hostname)\n if (!resolved || resolved.status !== 'active') {\n throw new TenantResolutionError('This domain is not configured for any active organization', 404)\n }\n\n if (bodyTenantId && bodyTenantId !== resolved.tenantId) {\n throw new TenantResolutionError(\n 'tenantId in request body does not match the resolved custom domain',\n 400,\n )\n }\n\n return {\n source: 'host',\n tenantId: resolved.tenantId,\n organizationId: resolved.organizationId,\n hostname,\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * Resolve the tenant context for a customer-portal request.\n *\n * - Platform-domain hosts: resolve the tenant server-side from the body\n * `organizationId` (org \u2192 tenant). A legacy body `tenantId` is still accepted\n * and cross-checked against the resolved tenant (fail closed on mismatch); if\n * no `organizationId` is supplied it falls back to the legacy `tenantId`.\n * - Custom-domain hosts: resolve via `domainMappingService.resolveByHostname()`.\n * If the body supplied a different `tenantId`, fail closed (mismatch).\n *\n * This is the single entry point used by all customer-auth routes (login,\n * signup, magic-link, password-reset) so they all behave consistently when\n * the request arrives on a tenant's branded URL.\n *\n * See `.ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md` Phase 1.5 and\n * `.ai/specs/implemented/2026-06-05-tenant-ownership-and-module-acl-authorization.md` \u00A7 C.\n */\n\nimport { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\n\nexport class TenantResolutionError extends Error {\n constructor(message: string, public readonly status: number) {\n super(message)\n this.name = 'TenantResolutionError'\n }\n}\n\nexport type ResolvedTenantContext = {\n source: 'body' | 'host'\n tenantId: string\n organizationId: string | null\n hostname: string | null\n}\n\nfunction readForcedHost(req: Request): string | null {\n // Test-only override. The middleware honors `X-Force-Host` only when\n // `NODE_ENV === 'test'` AND `X-Force-Host-Secret` matches; we mirror the\n // same check here so request-scoped helpers behave the same way under tests.\n if (process.env.NODE_ENV !== 'test') return null\n const expected = process.env.FORCE_HOST_SECRET\n if (!expected) return null\n if (!secretEqual(req.headers.get('x-force-host-secret'), expected)) return null\n const host = req.headers.get('x-force-host')\n return host && host.length > 0 ? host : null\n}\n\nasync function resolveTenantFromOrganization(\n organizationId: string,\n container: AppContainer | undefined,\n): Promise<{ tenantId: string; organizationId: string }> {\n let em: EntityManager\n if (container) {\n em = container.resolve('em') as EntityManager\n } else {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const requestContainer = await createRequestContainer()\n em = requestContainer.resolve('em') as EntityManager\n }\n\n const organization = await em.findOne(\n Organization,\n { id: organizationId, deletedAt: null },\n { populate: ['tenant'] },\n )\n if (!organization) {\n throw new TenantResolutionError('Organization not found', 400)\n }\n const tenantId = typeof organization.tenant === 'string'\n ? organization.tenant\n : organization.tenant?.id\n ? String(organization.tenant.id)\n : null\n if (!tenantId) {\n throw new TenantResolutionError('Organization not found', 400)\n }\n return { tenantId, organizationId: String(organization.id) }\n}\n\nexport async function resolveTenantContext(\n req: Request,\n bodyTenantId: string | null | undefined,\n options?: { container?: AppContainer; organizationId?: string | null },\n): Promise<ResolvedTenantContext> {\n const rawHost = readForcedHost(req) ?? req.headers.get('host')\n const hostname = rawHost ? tryNormalizeHostname(rawHost) : null\n const isPlatform = hostname ? platformDomains().includes(hostname) : true\n const bodyOrganizationId = options?.organizationId ?? null\n\n if (isPlatform) {\n if (bodyOrganizationId) {\n const resolved = await resolveTenantFromOrganization(bodyOrganizationId, options?.container)\n if (bodyTenantId && bodyTenantId !== resolved.tenantId) {\n throw new TenantResolutionError(\n 'tenantId in request body does not match the resolved organization',\n 400,\n )\n }\n return {\n source: 'body',\n tenantId: resolved.tenantId,\n organizationId: resolved.organizationId,\n hostname,\n }\n }\n if (!bodyTenantId) {\n throw new TenantResolutionError(\n 'organizationId or tenantId is required for platform-domain logins',\n 400,\n )\n }\n return { source: 'body', tenantId: bodyTenantId, organizationId: null, hostname }\n }\n\n // Custom-domain host\n let service: DomainMappingService | null = null\n try {\n if (options?.container) {\n service = options.container.resolve('domainMappingService') as DomainMappingService\n } else {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n service = container.resolve('domainMappingService') as DomainMappingService\n }\n } catch {\n throw new TenantResolutionError('Custom domain routing is not available on this deployment', 503)\n }\n\n if (!hostname) throw new TenantResolutionError('Unable to determine request hostname', 400)\n const resolved = await service.resolveByHostname(hostname)\n if (!resolved || resolved.status !== 'active') {\n throw new TenantResolutionError('This domain is not configured for any active organization', 404)\n }\n\n if (bodyTenantId && bodyTenantId !== resolved.tenantId) {\n throw new TenantResolutionError(\n 'tenantId in request body does not match the resolved custom domain',\n 400,\n )\n }\n\n return {\n source: 'host',\n tenantId: resolved.tenantId,\n organizationId: resolved.organizationId,\n hostname,\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["resolved"]
7
7
  }
@@ -89,7 +89,7 @@ const features = [
89
89
  // privacy model is strict owner-only with NO admin bypass, so this feature is
90
90
  // declared but INERT — granting it does not unlock other users' private emails
91
91
  // (the visibility filter and the visibility-change gate ignore it). See
92
- // .ai/specs/2026-05-27-crm-email-integration.md (v1 strict owner-only).
92
+ // .ai/specs/implemented/2026-05-27-crm-email-integration.md (v1 strict owner-only).
93
93
  {
94
94
  id: "customers.email.view_private",
95
95
  title: "View other users' private emails (reserved \u2014 inert in v1)",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/customers/acl.ts"],
4
- "sourcesContent": ["export const features = [\n { id: 'customers.people.view', title: 'View people', module: 'customers' },\n {\n id: 'customers.people.manage',\n title: 'Manage people',\n module: 'customers',\n dependsOn: ['customers.people.view'],\n },\n { id: 'customers.companies.view', title: 'View companies', module: 'customers' },\n {\n id: 'customers.companies.manage',\n title: 'Manage companies',\n module: 'customers',\n dependsOn: ['customers.companies.view'],\n },\n {\n id: 'customers.deals.view',\n title: 'View deals',\n module: 'customers',\n dependsOn: ['customers.people.view'],\n },\n {\n id: 'customers.deals.manage',\n title: 'Manage deals',\n module: 'customers',\n dependsOn: ['customers.deals.view'],\n },\n { id: 'customers.activities.view', title: 'View activities', module: 'customers' },\n {\n id: 'customers.activities.manage',\n title: 'Manage activities',\n module: 'customers',\n dependsOn: ['customers.activities.view'],\n },\n { id: 'customers.settings.manage', title: 'Manage customer settings', module: 'customers' },\n { id: 'customers.pipelines.view', title: 'View pipelines', module: 'customers' },\n {\n id: 'customers.pipelines.manage',\n title: 'Manage pipelines',\n module: 'customers',\n dependsOn: ['customers.pipelines.view'],\n },\n {\n id: 'customers.widgets.todos',\n title: 'Use customer todos widget',\n module: 'customers',\n dependsOn: ['customers.activities.view'],\n },\n {\n id: 'customers.widgets.next-interactions',\n title: 'Use customer next interactions widget',\n module: 'customers',\n dependsOn: ['customers.interactions.view'],\n },\n {\n id: 'customers.widgets.new-customers',\n title: 'Use customer new customers widget',\n module: 'customers',\n dependsOn: ['customers.people.view'],\n },\n {\n id: 'customers.widgets.new-deals',\n title: 'Use customer new deals widget',\n module: 'customers',\n dependsOn: ['customers.deals.view'],\n },\n { id: 'customers.interactions.view', title: 'View interactions', module: 'customers' },\n {\n id: 'customers.interactions.manage',\n title: 'Manage interactions',\n module: 'customers',\n dependsOn: ['customers.interactions.view'],\n },\n { id: 'customers.roles.view', title: 'View entity roles', module: 'customers' },\n {\n id: 'customers.roles.manage',\n title: 'Manage entity roles',\n module: 'customers',\n dependsOn: ['customers.roles.view'],\n },\n // Email integration (2026-05-27)\n {\n id: 'customers.email.compose',\n title: 'Compose / send emails from CRM',\n module: 'customers',\n dependsOn: ['customers.people.view'],\n },\n // Reserved for a future v2 admin-oversight capability. In v1 the email\n // privacy model is strict owner-only with NO admin bypass, so this feature is\n // declared but INERT \u2014 granting it does not unlock other users' private emails\n // (the visibility filter and the visibility-change gate ignore it). See\n // .ai/specs/2026-05-27-crm-email-integration.md (v1 strict owner-only).\n {\n id: 'customers.email.view_private',\n title: 'View other users\\' private emails (reserved \u2014 inert in v1)',\n module: 'customers',\n dependsOn: ['customers.interactions.view'],\n },\n]\n\nexport default features\n"],
4
+ "sourcesContent": ["export const features = [\n { id: 'customers.people.view', title: 'View people', module: 'customers' },\n {\n id: 'customers.people.manage',\n title: 'Manage people',\n module: 'customers',\n dependsOn: ['customers.people.view'],\n },\n { id: 'customers.companies.view', title: 'View companies', module: 'customers' },\n {\n id: 'customers.companies.manage',\n title: 'Manage companies',\n module: 'customers',\n dependsOn: ['customers.companies.view'],\n },\n {\n id: 'customers.deals.view',\n title: 'View deals',\n module: 'customers',\n dependsOn: ['customers.people.view'],\n },\n {\n id: 'customers.deals.manage',\n title: 'Manage deals',\n module: 'customers',\n dependsOn: ['customers.deals.view'],\n },\n { id: 'customers.activities.view', title: 'View activities', module: 'customers' },\n {\n id: 'customers.activities.manage',\n title: 'Manage activities',\n module: 'customers',\n dependsOn: ['customers.activities.view'],\n },\n { id: 'customers.settings.manage', title: 'Manage customer settings', module: 'customers' },\n { id: 'customers.pipelines.view', title: 'View pipelines', module: 'customers' },\n {\n id: 'customers.pipelines.manage',\n title: 'Manage pipelines',\n module: 'customers',\n dependsOn: ['customers.pipelines.view'],\n },\n {\n id: 'customers.widgets.todos',\n title: 'Use customer todos widget',\n module: 'customers',\n dependsOn: ['customers.activities.view'],\n },\n {\n id: 'customers.widgets.next-interactions',\n title: 'Use customer next interactions widget',\n module: 'customers',\n dependsOn: ['customers.interactions.view'],\n },\n {\n id: 'customers.widgets.new-customers',\n title: 'Use customer new customers widget',\n module: 'customers',\n dependsOn: ['customers.people.view'],\n },\n {\n id: 'customers.widgets.new-deals',\n title: 'Use customer new deals widget',\n module: 'customers',\n dependsOn: ['customers.deals.view'],\n },\n { id: 'customers.interactions.view', title: 'View interactions', module: 'customers' },\n {\n id: 'customers.interactions.manage',\n title: 'Manage interactions',\n module: 'customers',\n dependsOn: ['customers.interactions.view'],\n },\n { id: 'customers.roles.view', title: 'View entity roles', module: 'customers' },\n {\n id: 'customers.roles.manage',\n title: 'Manage entity roles',\n module: 'customers',\n dependsOn: ['customers.roles.view'],\n },\n // Email integration (2026-05-27)\n {\n id: 'customers.email.compose',\n title: 'Compose / send emails from CRM',\n module: 'customers',\n dependsOn: ['customers.people.view'],\n },\n // Reserved for a future v2 admin-oversight capability. In v1 the email\n // privacy model is strict owner-only with NO admin bypass, so this feature is\n // declared but INERT \u2014 granting it does not unlock other users' private emails\n // (the visibility filter and the visibility-change gate ignore it). See\n // .ai/specs/implemented/2026-05-27-crm-email-integration.md (v1 strict owner-only).\n {\n id: 'customers.email.view_private',\n title: 'View other users\\' private emails (reserved \u2014 inert in v1)',\n module: 'customers',\n dependsOn: ['customers.interactions.view'],\n },\n]\n\nexport default features\n"],
5
5
  "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,yBAAyB,OAAO,eAAe,QAAQ,YAAY;AAAA,EACzE;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,uBAAuB;AAAA,EACrC;AAAA,EACA,EAAE,IAAI,4BAA4B,OAAO,kBAAkB,QAAQ,YAAY;AAAA,EAC/E;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,sBAAsB;AAAA,EACpC;AAAA,EACA,EAAE,IAAI,6BAA6B,OAAO,mBAAmB,QAAQ,YAAY;AAAA,EACjF;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,2BAA2B;AAAA,EACzC;AAAA,EACA,EAAE,IAAI,6BAA6B,OAAO,4BAA4B,QAAQ,YAAY;AAAA,EAC1F,EAAE,IAAI,4BAA4B,OAAO,kBAAkB,QAAQ,YAAY;AAAA,EAC/E;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,2BAA2B;AAAA,EACzC;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,6BAA6B;AAAA,EAC3C;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,sBAAsB;AAAA,EACpC;AAAA,EACA,EAAE,IAAI,+BAA+B,OAAO,qBAAqB,QAAQ,YAAY;AAAA,EACrF;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,6BAA6B;AAAA,EAC3C;AAAA,EACA,EAAE,IAAI,wBAAwB,OAAO,qBAAqB,QAAQ,YAAY;AAAA,EAC9E;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,sBAAsB;AAAA,EACpC;AAAA;AAAA,EAEA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,uBAAuB;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,6BAA6B;AAAA,EAC3C;AACF;AAEA,IAAO,cAAQ;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/ai-tools/companies-pack.ts"],
4
- "sourcesContent": ["/**\n * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).\n *\n * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_companies` is now an API-backed wrapper over\n * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_company` to a single\n * in-process call to `GET /api/customers/companies/<id>?include=...` over the\n * documented aggregate detail route. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n */\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\n\nconst listCompaniesInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against display name / email / domain. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to companies carrying at least one of these tag ids.'),\n })\n .passthrough()\n\ntype ListCompaniesInput = z.infer<typeof listCompaniesInput>\n\ntype ListCompaniesApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n domain?: string | null\n website_url?: string | null\n websiteUrl?: string | null\n industry?: string | null\n size_bucket?: string | null\n sizeBucket?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListCompaniesApiResponse = {\n items?: ListCompaniesApiItem[]\n total?: number\n}\n\ntype ListCompaniesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listCompaniesTool = defineApiBackedAiTool<\n ListCompaniesInput,\n ListCompaniesApiResponse,\n ListCompaniesOutput\n>({\n name: 'customers.list_companies',\n displayName: 'List companies',\n description:\n 'Search / list companies for the caller tenant + organization. Returns { items, total, limit, offset }.',\n inputSchema: listCompaniesInput,\n requiredFeatures: ['customers.companies.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/companies',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListCompaniesApiResponse\n const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n displayName: row.display_name ?? row.displayName ?? null,\n primaryEmail: row.primary_email ?? row.primaryEmail ?? null,\n primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,\n status: row.status ?? null,\n lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,\n source: row.source ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n domain: row.domain ?? null,\n websiteUrl: row.website_url ?? row.websiteUrl ?? null,\n industry: row.industry ?? null,\n sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getCompanyInput = z.object({\n companyId: z.string().uuid().describe('Company entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetCompanyInput = z.infer<typeof getCompanyInput>\n\nfunction toIsoCompany(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getCompanyTool: CustomersAiToolDefinition = {\n name: 'customers.get_company',\n displayName: 'Get company',\n description:\n 'Fetch a company customer record by id with profile fields and (optionally) notes, activities, deals, people, addresses, tasks, tags, and custom fields. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getCompanyInput,\n requiredFeatures: ['customers.companies.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetCompanyInput = getCompanyInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/companies/${input.companyId}`,\n }\n if (includeRelated) {\n operation.query = {\n include: 'addresses,comments,activities,interactions,deals,todos,people',\n }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, companyId: input.companyId }\n }\n throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const companyRow = (data.company ?? null) as Record<string, unknown> | null\n if (!companyRow) {\n return { found: false as const, companyId: input.companyId }\n }\n const profileRow = (data.profile ?? null) as Record<string, unknown> | null\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const addresses = Array.isArray(data.addresses) ? (data.addresses as Array<Record<string, unknown>>) : []\n const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []\n const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []\n const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []\n const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []\n const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []\n const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []\n const peopleRows = Array.isArray(data.people) ? (data.people as Array<Record<string, unknown>>) : []\n related = {\n addresses: addresses.map((address) => ({\n id: address.id,\n name: address.name ?? null,\n purpose: address.purpose ?? null,\n addressLine1: address.addressLine1 ?? null,\n addressLine2: address.addressLine2 ?? null,\n city: address.city ?? null,\n region: address.region ?? null,\n postalCode: address.postalCode ?? null,\n country: address.country ?? null,\n isPrimary: !!address.isPrimary,\n })),\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIsoCompany(activity.occurredAt),\n createdAt: toIsoCompany(activity.createdAt),\n })),\n notes: notes.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? null,\n createdAt: toIsoCompany(comment.createdAt),\n })),\n tasks: todos.map((task) => ({\n id: task.id,\n todoId: task.todoId ?? task.id,\n todoSource: task.todoSource ?? null,\n createdAt: toIsoCompany(task.createdAt),\n })),\n interactions: interactions.map((interaction) => ({\n id: interaction.id,\n interactionType: interaction.interactionType,\n title: interaction.title ?? null,\n status: interaction.status,\n scheduledAt: toIsoCompany(interaction.scheduledAt),\n occurredAt: toIsoCompany(interaction.occurredAt),\n })),\n tags: tagsRows\n .map((tag) => {\n if (!tag || typeof tag !== 'object') return null\n const id = typeof tag.id === 'string' ? tag.id : null\n const label = typeof tag.label === 'string' ? tag.label : null\n if (!id || !label) return null\n const slug = typeof tag.slug === 'string' ? tag.slug : label\n const color = typeof tag.color === 'string' ? tag.color : null\n return { id, slug, label, color }\n })\n .filter(\n (entry): entry is { id: string; slug: string; label: string; color: string | null } =>\n entry !== null,\n ),\n deals: dealsRows\n .map((deal) => {\n if (!deal || typeof deal !== 'object') return null\n const id = typeof deal.id === 'string' ? deal.id : null\n if (!id) return null\n return {\n id,\n title: typeof deal.title === 'string' ? deal.title : '',\n status: typeof deal.status === 'string' ? deal.status : null,\n pipelineStageId:\n typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,\n valueAmount:\n typeof deal.valueAmount === 'string'\n ? deal.valueAmount\n : deal.valueAmount === null || deal.valueAmount === undefined\n ? null\n : String(deal.valueAmount),\n valueCurrency:\n typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,\n }\n })\n .filter(\n (\n value,\n ): value is {\n id: string\n title: string\n status: string | null\n pipelineStageId: string | null\n valueAmount: string | null\n valueCurrency: string | null\n } => value !== null,\n ),\n people: peopleRows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n const displayName = typeof person.displayName === 'string' ? person.displayName : null\n if (!id || !displayName) return null\n return {\n id,\n displayName,\n primaryEmail:\n typeof person.primaryEmail === 'string' ? person.primaryEmail : null,\n primaryPhone:\n typeof person.primaryPhone === 'string' ? person.primaryPhone : null,\n jobTitle: typeof person.jobTitle === 'string' ? person.jobTitle : null,\n department: typeof person.department === 'string' ? person.department : null,\n }\n })\n .filter(\n (\n value,\n ): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n jobTitle: string | null\n department: string | null\n } => value !== null,\n ),\n }\n }\n return {\n found: true as const,\n company: {\n id: companyRow.id,\n displayName: companyRow.displayName ?? null,\n description: companyRow.description ?? null,\n primaryEmail: companyRow.primaryEmail ?? null,\n primaryPhone: companyRow.primaryPhone ?? null,\n status: companyRow.status ?? null,\n lifecycleStage: companyRow.lifecycleStage ?? null,\n source: companyRow.source ?? null,\n ownerUserId: companyRow.ownerUserId ?? null,\n organizationId: companyRow.organizationId ?? null,\n tenantId: companyRow.tenantId ?? null,\n createdAt: toIsoCompany(companyRow.createdAt),\n updatedAt: toIsoCompany(companyRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n legalName: profileRow.legalName ?? null,\n brandName: profileRow.brandName ?? null,\n domain: profileRow.domain ?? null,\n websiteUrl: profileRow.websiteUrl ?? null,\n industry: profileRow.industry ?? null,\n sizeBucket: profileRow.sizeBucket ?? null,\n annualRevenue: profileRow.annualRevenue ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]\n\nexport default companiesAiTools\n"],
4
+ "sourcesContent": ["/**\n * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_companies` is now an API-backed wrapper over\n * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_company` to a single\n * in-process call to `GET /api/customers/companies/<id>?include=...` over the\n * documented aggregate detail route. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n */\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\n\nconst listCompaniesInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against display name / email / domain. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to companies carrying at least one of these tag ids.'),\n })\n .passthrough()\n\ntype ListCompaniesInput = z.infer<typeof listCompaniesInput>\n\ntype ListCompaniesApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n domain?: string | null\n website_url?: string | null\n websiteUrl?: string | null\n industry?: string | null\n size_bucket?: string | null\n sizeBucket?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListCompaniesApiResponse = {\n items?: ListCompaniesApiItem[]\n total?: number\n}\n\ntype ListCompaniesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listCompaniesTool = defineApiBackedAiTool<\n ListCompaniesInput,\n ListCompaniesApiResponse,\n ListCompaniesOutput\n>({\n name: 'customers.list_companies',\n displayName: 'List companies',\n description:\n 'Search / list companies for the caller tenant + organization. Returns { items, total, limit, offset }.',\n inputSchema: listCompaniesInput,\n requiredFeatures: ['customers.companies.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/companies',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListCompaniesApiResponse\n const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n displayName: row.display_name ?? row.displayName ?? null,\n primaryEmail: row.primary_email ?? row.primaryEmail ?? null,\n primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,\n status: row.status ?? null,\n lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,\n source: row.source ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n domain: row.domain ?? null,\n websiteUrl: row.website_url ?? row.websiteUrl ?? null,\n industry: row.industry ?? null,\n sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getCompanyInput = z.object({\n companyId: z.string().uuid().describe('Company entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetCompanyInput = z.infer<typeof getCompanyInput>\n\nfunction toIsoCompany(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getCompanyTool: CustomersAiToolDefinition = {\n name: 'customers.get_company',\n displayName: 'Get company',\n description:\n 'Fetch a company customer record by id with profile fields and (optionally) notes, activities, deals, people, addresses, tasks, tags, and custom fields. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getCompanyInput,\n requiredFeatures: ['customers.companies.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetCompanyInput = getCompanyInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/companies/${input.companyId}`,\n }\n if (includeRelated) {\n operation.query = {\n include: 'addresses,comments,activities,interactions,deals,todos,people',\n }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, companyId: input.companyId }\n }\n throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const companyRow = (data.company ?? null) as Record<string, unknown> | null\n if (!companyRow) {\n return { found: false as const, companyId: input.companyId }\n }\n const profileRow = (data.profile ?? null) as Record<string, unknown> | null\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const addresses = Array.isArray(data.addresses) ? (data.addresses as Array<Record<string, unknown>>) : []\n const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []\n const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []\n const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []\n const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []\n const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []\n const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []\n const peopleRows = Array.isArray(data.people) ? (data.people as Array<Record<string, unknown>>) : []\n related = {\n addresses: addresses.map((address) => ({\n id: address.id,\n name: address.name ?? null,\n purpose: address.purpose ?? null,\n addressLine1: address.addressLine1 ?? null,\n addressLine2: address.addressLine2 ?? null,\n city: address.city ?? null,\n region: address.region ?? null,\n postalCode: address.postalCode ?? null,\n country: address.country ?? null,\n isPrimary: !!address.isPrimary,\n })),\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIsoCompany(activity.occurredAt),\n createdAt: toIsoCompany(activity.createdAt),\n })),\n notes: notes.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? null,\n createdAt: toIsoCompany(comment.createdAt),\n })),\n tasks: todos.map((task) => ({\n id: task.id,\n todoId: task.todoId ?? task.id,\n todoSource: task.todoSource ?? null,\n createdAt: toIsoCompany(task.createdAt),\n })),\n interactions: interactions.map((interaction) => ({\n id: interaction.id,\n interactionType: interaction.interactionType,\n title: interaction.title ?? null,\n status: interaction.status,\n scheduledAt: toIsoCompany(interaction.scheduledAt),\n occurredAt: toIsoCompany(interaction.occurredAt),\n })),\n tags: tagsRows\n .map((tag) => {\n if (!tag || typeof tag !== 'object') return null\n const id = typeof tag.id === 'string' ? tag.id : null\n const label = typeof tag.label === 'string' ? tag.label : null\n if (!id || !label) return null\n const slug = typeof tag.slug === 'string' ? tag.slug : label\n const color = typeof tag.color === 'string' ? tag.color : null\n return { id, slug, label, color }\n })\n .filter(\n (entry): entry is { id: string; slug: string; label: string; color: string | null } =>\n entry !== null,\n ),\n deals: dealsRows\n .map((deal) => {\n if (!deal || typeof deal !== 'object') return null\n const id = typeof deal.id === 'string' ? deal.id : null\n if (!id) return null\n return {\n id,\n title: typeof deal.title === 'string' ? deal.title : '',\n status: typeof deal.status === 'string' ? deal.status : null,\n pipelineStageId:\n typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,\n valueAmount:\n typeof deal.valueAmount === 'string'\n ? deal.valueAmount\n : deal.valueAmount === null || deal.valueAmount === undefined\n ? null\n : String(deal.valueAmount),\n valueCurrency:\n typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,\n }\n })\n .filter(\n (\n value,\n ): value is {\n id: string\n title: string\n status: string | null\n pipelineStageId: string | null\n valueAmount: string | null\n valueCurrency: string | null\n } => value !== null,\n ),\n people: peopleRows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n const displayName = typeof person.displayName === 'string' ? person.displayName : null\n if (!id || !displayName) return null\n return {\n id,\n displayName,\n primaryEmail:\n typeof person.primaryEmail === 'string' ? person.primaryEmail : null,\n primaryPhone:\n typeof person.primaryPhone === 'string' ? person.primaryPhone : null,\n jobTitle: typeof person.jobTitle === 'string' ? person.jobTitle : null,\n department: typeof person.department === 'string' ? person.department : null,\n }\n })\n .filter(\n (\n value,\n ): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n jobTitle: string | null\n department: string | null\n } => value !== null,\n ),\n }\n }\n return {\n found: true as const,\n company: {\n id: companyRow.id,\n displayName: companyRow.displayName ?? null,\n description: companyRow.description ?? null,\n primaryEmail: companyRow.primaryEmail ?? null,\n primaryPhone: companyRow.primaryPhone ?? null,\n status: companyRow.status ?? null,\n lifecycleStage: companyRow.lifecycleStage ?? null,\n source: companyRow.source ?? null,\n ownerUserId: companyRow.ownerUserId ?? null,\n organizationId: companyRow.organizationId ?? null,\n tenantId: companyRow.tenantId ?? null,\n createdAt: toIsoCompany(companyRow.createdAt),\n updatedAt: toIsoCompany(companyRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n legalName: profileRow.legalName ?? null,\n brandName: profileRow.brandName ?? null,\n domain: profileRow.domain ?? null,\n websiteUrl: profileRow.websiteUrl ?? null,\n industry: profileRow.industry ?? null,\n sizeBucket: profileRow.sizeBucket ?? null,\n annualRevenue: profileRow.annualRevenue ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]\n\nexport default companiesAiTools\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }