@open-mercato/core 0.6.8-develop.6993.1.3fc50f3409 → 0.6.8-develop.7000.1.0ae682bad1

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 (708) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/sync_run/index.js +2 -0
  3. package/dist/generated/entities/sync_run/index.js.map +2 -2
  4. package/dist/generated/entities/warranty_claim/index.js +105 -0
  5. package/dist/generated/entities/warranty_claim/index.js.map +7 -0
  6. package/dist/generated/entities/warranty_claim_event/index.js +25 -0
  7. package/dist/generated/entities/warranty_claim_event/index.js.map +7 -0
  8. package/dist/generated/entities/warranty_claim_line/index.js +77 -0
  9. package/dist/generated/entities/warranty_claim_line/index.js.map +7 -0
  10. package/dist/generated/entities/warranty_claim_registration/index.js +43 -0
  11. package/dist/generated/entities/warranty_claim_registration/index.js.map +7 -0
  12. package/dist/generated/entities/warranty_claim_sequence/index.js +17 -0
  13. package/dist/generated/entities/warranty_claim_sequence/index.js.map +7 -0
  14. package/dist/generated/entities/warranty_claim_settings/index.js +43 -0
  15. package/dist/generated/entities/warranty_claim_settings/index.js.map +7 -0
  16. package/dist/generated/entities/warranty_claim_sla_signal/index.js +25 -0
  17. package/dist/generated/entities/warranty_claim_sla_signal/index.js.map +7 -0
  18. package/dist/generated/entities/warranty_troubleshooting_guide/index.js +25 -0
  19. package/dist/generated/entities/warranty_troubleshooting_guide/index.js.map +7 -0
  20. package/dist/generated/entities/warranty_vendor_policy/index.js +31 -0
  21. package/dist/generated/entities/warranty_vendor_policy/index.js.map +7 -0
  22. package/dist/generated/entities.ids.generated.js +12 -0
  23. package/dist/generated/entities.ids.generated.js.map +2 -2
  24. package/dist/generated/entity-fields-registry.js +201 -0
  25. package/dist/generated/entity-fields-registry.js.map +2 -2
  26. package/dist/helpers/integration/api.js +3 -2
  27. package/dist/helpers/integration/api.js.map +2 -2
  28. package/dist/helpers/integration/salesFixtures.js +13 -6
  29. package/dist/helpers/integration/salesFixtures.js.map +2 -2
  30. package/dist/modules/attachments/di.js +18 -0
  31. package/dist/modules/attachments/di.js.map +2 -2
  32. package/dist/modules/attachments/fields/attachment.js +186 -26
  33. package/dist/modules/attachments/fields/attachment.js.map +2 -2
  34. package/dist/modules/attachments/lib/scoped-upload-service.js +202 -0
  35. package/dist/modules/attachments/lib/scoped-upload-service.js.map +7 -0
  36. package/dist/modules/attachments/lib/target-access-service.js +39 -0
  37. package/dist/modules/attachments/lib/target-access-service.js.map +7 -0
  38. package/dist/modules/auth/api/sidebar/preferences/route.js +2 -2
  39. package/dist/modules/auth/api/sidebar/preferences/route.js.map +1 -1
  40. package/dist/modules/auth/lib/backendChrome.js +3 -3
  41. package/dist/modules/auth/lib/backendChrome.js.map +1 -1
  42. package/dist/modules/auth/services/sidebarPreferencesService.js +7 -2
  43. package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
  44. package/dist/modules/customers/api/companies/route.js +1 -0
  45. package/dist/modules/customers/api/companies/route.js.map +2 -2
  46. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +2 -2
  47. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  48. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +2 -2
  49. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  50. package/dist/modules/customers/components/detail/assignableStaff.js +6 -69
  51. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  52. package/dist/modules/customers/lib/assignableStaff.js +76 -0
  53. package/dist/modules/customers/lib/assignableStaff.js.map +7 -0
  54. package/dist/modules/data_sync/api/options.js +1 -0
  55. package/dist/modules/data_sync/api/options.js.map +2 -2
  56. package/dist/modules/data_sync/api/run.js +15 -1
  57. package/dist/modules/data_sync/api/run.js.map +2 -2
  58. package/dist/modules/data_sync/api/runs/[id]/retry.js +23 -1
  59. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  60. package/dist/modules/data_sync/api/runs/[id]/route.js +1 -0
  61. package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
  62. package/dist/modules/data_sync/backend/data-sync/page.js +60 -17
  63. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  64. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +34 -2
  65. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  66. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +21 -15
  67. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  68. package/dist/modules/data_sync/components/RunParameterFields.js +135 -0
  69. package/dist/modules/data_sync/components/RunParameterFields.js.map +7 -0
  70. package/dist/modules/data_sync/data/entities.js +3 -0
  71. package/dist/modules/data_sync/data/entities.js.map +2 -2
  72. package/dist/modules/data_sync/data/validators.js +4 -1
  73. package/dist/modules/data_sync/data/validators.js.map +2 -2
  74. package/dist/modules/data_sync/lib/batch-stream.js +56 -0
  75. package/dist/modules/data_sync/lib/batch-stream.js.map +7 -0
  76. package/dist/modules/data_sync/lib/run-parameters.js +108 -0
  77. package/dist/modules/data_sync/lib/run-parameters.js.map +7 -0
  78. package/dist/modules/data_sync/lib/start-run.js +1 -0
  79. package/dist/modules/data_sync/lib/start-run.js.map +2 -2
  80. package/dist/modules/data_sync/lib/sync-engine.js +165 -101
  81. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  82. package/dist/modules/data_sync/lib/sync-run-service.js +1 -0
  83. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  84. package/dist/modules/data_sync/migrations/Migration20260810120000.js +13 -0
  85. package/dist/modules/data_sync/migrations/Migration20260810120000.js.map +7 -0
  86. package/dist/modules/data_sync/workers/sync-scheduled.js +23 -2
  87. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  88. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +4 -0
  89. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
  90. package/dist/modules/inbox_ops/api/extract/route.js +4 -0
  91. package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
  92. package/dist/modules/inbox_ops/api/webhook/inbound.js +4 -0
  93. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  94. package/dist/modules/query_index/di.js +125 -48
  95. package/dist/modules/query_index/di.js.map +2 -2
  96. package/dist/modules/sales/commands/documents.js +34 -13
  97. package/dist/modules/sales/commands/documents.js.map +2 -2
  98. package/dist/modules/warranty_claims/acl.js +75 -0
  99. package/dist/modules/warranty_claims/acl.js.map +7 -0
  100. package/dist/modules/warranty_claims/ai-agents.js +161 -0
  101. package/dist/modules/warranty_claims/ai-agents.js.map +7 -0
  102. package/dist/modules/warranty_claims/ai-tools.js +591 -0
  103. package/dist/modules/warranty_claims/ai-tools.js.map +7 -0
  104. package/dist/modules/warranty_claims/api/ai/assess/route.js +304 -0
  105. package/dist/modules/warranty_claims/api/ai/assess/route.js.map +7 -0
  106. package/dist/modules/warranty_claims/api/ai/draft-reply/route.js +127 -0
  107. package/dist/modules/warranty_claims/api/ai/draft-reply/route.js.map +7 -0
  108. package/dist/modules/warranty_claims/api/ai/suggest/route.js +177 -0
  109. package/dist/modules/warranty_claims/api/ai/suggest/route.js.map +7 -0
  110. package/dist/modules/warranty_claims/api/assign/route.js +124 -0
  111. package/dist/modules/warranty_claims/api/assign/route.js.map +7 -0
  112. package/dist/modules/warranty_claims/api/assignees/route.js +97 -0
  113. package/dist/modules/warranty_claims/api/assignees/route.js.map +7 -0
  114. package/dist/modules/warranty_claims/api/credit-memo/route.js +152 -0
  115. package/dist/modules/warranty_claims/api/credit-memo/route.js.map +7 -0
  116. package/dist/modules/warranty_claims/api/entitlement/route.js +145 -0
  117. package/dist/modules/warranty_claims/api/entitlement/route.js.map +7 -0
  118. package/dist/modules/warranty_claims/api/events/route.js +199 -0
  119. package/dist/modules/warranty_claims/api/events/route.js.map +7 -0
  120. package/dist/modules/warranty_claims/api/external/claims/route.js +446 -0
  121. package/dist/modules/warranty_claims/api/external/claims/route.js.map +7 -0
  122. package/dist/modules/warranty_claims/api/lines/route.js +258 -0
  123. package/dist/modules/warranty_claims/api/lines/route.js.map +7 -0
  124. package/dist/modules/warranty_claims/api/openapi.js +27 -0
  125. package/dist/modules/warranty_claims/api/openapi.js.map +7 -0
  126. package/dist/modules/warranty_claims/api/portal/attachments/route.js +490 -0
  127. package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +7 -0
  128. package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js +178 -0
  129. package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js.map +7 -0
  130. package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js +101 -0
  131. package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js.map +7 -0
  132. package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +78 -0
  133. package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +7 -0
  134. package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +81 -0
  135. package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +7 -0
  136. package/dist/modules/warranty_claims/api/portal/claims/route.js +462 -0
  137. package/dist/modules/warranty_claims/api/portal/claims/route.js.map +7 -0
  138. package/dist/modules/warranty_claims/api/portal/events/route.js +228 -0
  139. package/dist/modules/warranty_claims/api/portal/events/route.js.map +7 -0
  140. package/dist/modules/warranty_claims/api/portal/options/route.js +91 -0
  141. package/dist/modules/warranty_claims/api/portal/options/route.js.map +7 -0
  142. package/dist/modules/warranty_claims/api/portal/orders/lines/route.js +172 -0
  143. package/dist/modules/warranty_claims/api/portal/orders/lines/route.js.map +7 -0
  144. package/dist/modules/warranty_claims/api/portal/orders/route.js +151 -0
  145. package/dist/modules/warranty_claims/api/portal/orders/route.js.map +7 -0
  146. package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js +121 -0
  147. package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js.map +7 -0
  148. package/dist/modules/warranty_claims/api/receiving/route.js +176 -0
  149. package/dist/modules/warranty_claims/api/receiving/route.js.map +7 -0
  150. package/dist/modules/warranty_claims/api/registrations/route.js +382 -0
  151. package/dist/modules/warranty_claims/api/registrations/route.js.map +7 -0
  152. package/dist/modules/warranty_claims/api/replacement-order/route.js +150 -0
  153. package/dist/modules/warranty_claims/api/replacement-order/route.js.map +7 -0
  154. package/dist/modules/warranty_claims/api/return-label/route.js +235 -0
  155. package/dist/modules/warranty_claims/api/return-label/route.js.map +7 -0
  156. package/dist/modules/warranty_claims/api/risk/route.js +101 -0
  157. package/dist/modules/warranty_claims/api/risk/route.js.map +7 -0
  158. package/dist/modules/warranty_claims/api/route.js +482 -0
  159. package/dist/modules/warranty_claims/api/route.js.map +7 -0
  160. package/dist/modules/warranty_claims/api/sales-return/route.js +151 -0
  161. package/dist/modules/warranty_claims/api/sales-return/route.js.map +7 -0
  162. package/dist/modules/warranty_claims/api/settings-general/route.js +195 -0
  163. package/dist/modules/warranty_claims/api/settings-general/route.js.map +7 -0
  164. package/dist/modules/warranty_claims/api/stats/route.js +199 -0
  165. package/dist/modules/warranty_claims/api/stats/route.js.map +7 -0
  166. package/dist/modules/warranty_claims/api/submit/route.js +120 -0
  167. package/dist/modules/warranty_claims/api/submit/route.js.map +7 -0
  168. package/dist/modules/warranty_claims/api/transition/route.js +126 -0
  169. package/dist/modules/warranty_claims/api/transition/route.js.map +7 -0
  170. package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js +261 -0
  171. package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js.map +7 -0
  172. package/dist/modules/warranty_claims/api/vendor-policies/route.js +311 -0
  173. package/dist/modules/warranty_claims/api/vendor-policies/route.js.map +7 -0
  174. package/dist/modules/warranty_claims/api/vendor-recovery/route.js +130 -0
  175. package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +7 -0
  176. package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +153 -0
  177. package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +7 -0
  178. package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js +268 -0
  179. package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js.map +7 -0
  180. package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js +54 -0
  181. package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js.map +7 -0
  182. package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js +87 -0
  183. package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js.map +7 -0
  184. package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js +120 -0
  185. package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js.map +7 -0
  186. package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js +135 -0
  187. package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js.map +7 -0
  188. package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js +218 -0
  189. package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js.map +7 -0
  190. package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js +231 -0
  191. package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js.map +7 -0
  192. package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js +82 -0
  193. package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js.map +7 -0
  194. package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js +136 -0
  195. package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js.map +7 -0
  196. package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js +41 -0
  197. package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js.map +7 -0
  198. package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js +45 -0
  199. package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js.map +7 -0
  200. package/dist/modules/warranty_claims/backend/components/claimSla.js +92 -0
  201. package/dist/modules/warranty_claims/backend/components/claimSla.js.map +7 -0
  202. package/dist/modules/warranty_claims/backend/components/orderLookup.js +81 -0
  203. package/dist/modules/warranty_claims/backend/components/orderLookup.js.map +7 -0
  204. package/dist/modules/warranty_claims/backend/components/productLookup.js +317 -0
  205. package/dist/modules/warranty_claims/backend/components/productLookup.js.map +7 -0
  206. package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js +61 -0
  207. package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js.map +7 -0
  208. package/dist/modules/warranty_claims/backend/page.js +1088 -0
  209. package/dist/modules/warranty_claims/backend/page.js.map +7 -0
  210. package/dist/modules/warranty_claims/backend/page.meta.js +16 -0
  211. package/dist/modules/warranty_claims/backend/page.meta.js.map +7 -0
  212. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js +427 -0
  213. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js.map +7 -0
  214. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js +16 -0
  215. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js.map +7 -0
  216. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +2101 -0
  217. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +7 -0
  218. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js +16 -0
  219. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js.map +7 -0
  220. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js +1249 -0
  221. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js.map +7 -0
  222. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js +16 -0
  223. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js.map +7 -0
  224. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js +110 -0
  225. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js.map +7 -0
  226. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js +17 -0
  227. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js.map +7 -0
  228. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js +49 -0
  229. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js.map +7 -0
  230. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js +17 -0
  231. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js.map +7 -0
  232. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js +485 -0
  233. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js.map +7 -0
  234. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js +19 -0
  235. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js.map +7 -0
  236. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js +253 -0
  237. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js.map +7 -0
  238. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js +1565 -0
  239. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js.map +7 -0
  240. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js +19 -0
  241. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js.map +7 -0
  242. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js +472 -0
  243. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js.map +7 -0
  244. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js +115 -0
  245. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js.map +7 -0
  246. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js +19 -0
  247. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js.map +7 -0
  248. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js +54 -0
  249. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js.map +7 -0
  250. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js +19 -0
  251. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js.map +7 -0
  252. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js +414 -0
  253. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js.map +7 -0
  254. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js +21 -0
  255. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js.map +7 -0
  256. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js +171 -0
  257. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js.map +7 -0
  258. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js +111 -0
  259. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js.map +7 -0
  260. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js +19 -0
  261. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js.map +7 -0
  262. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js +48 -0
  263. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js.map +7 -0
  264. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js +19 -0
  265. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js.map +7 -0
  266. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js +429 -0
  267. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js.map +7 -0
  268. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js +21 -0
  269. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js.map +7 -0
  270. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js +182 -0
  271. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js.map +7 -0
  272. package/dist/modules/warranty_claims/ce.js +48 -0
  273. package/dist/modules/warranty_claims/ce.js.map +7 -0
  274. package/dist/modules/warranty_claims/commands/claim-lines.js +911 -0
  275. package/dist/modules/warranty_claims/commands/claim-lines.js.map +7 -0
  276. package/dist/modules/warranty_claims/commands/claims.js +2577 -0
  277. package/dist/modules/warranty_claims/commands/claims.js.map +7 -0
  278. package/dist/modules/warranty_claims/commands/index.js +4 -0
  279. package/dist/modules/warranty_claims/commands/index.js.map +7 -0
  280. package/dist/modules/warranty_claims/commands/settings.js +169 -0
  281. package/dist/modules/warranty_claims/commands/settings.js.map +7 -0
  282. package/dist/modules/warranty_claims/commands/shared.js +162 -0
  283. package/dist/modules/warranty_claims/commands/shared.js.map +7 -0
  284. package/dist/modules/warranty_claims/data/constants.js +35 -0
  285. package/dist/modules/warranty_claims/data/constants.js.map +7 -0
  286. package/dist/modules/warranty_claims/data/enrichers.js +97 -0
  287. package/dist/modules/warranty_claims/data/enrichers.js.map +7 -0
  288. package/dist/modules/warranty_claims/data/entities.js +731 -0
  289. package/dist/modules/warranty_claims/data/entities.js.map +7 -0
  290. package/dist/modules/warranty_claims/data/validators.js +488 -0
  291. package/dist/modules/warranty_claims/data/validators.js.map +7 -0
  292. package/dist/modules/warranty_claims/di.js +29 -0
  293. package/dist/modules/warranty_claims/di.js.map +7 -0
  294. package/dist/modules/warranty_claims/encryption.js +28 -0
  295. package/dist/modules/warranty_claims/encryption.js.map +7 -0
  296. package/dist/modules/warranty_claims/events.js +81 -0
  297. package/dist/modules/warranty_claims/events.js.map +7 -0
  298. package/dist/modules/warranty_claims/extension-points.js +19 -0
  299. package/dist/modules/warranty_claims/extension-points.js.map +7 -0
  300. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +825 -0
  301. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +7 -0
  302. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js +11 -0
  303. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js.map +7 -0
  304. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js +1139 -0
  305. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js.map +7 -0
  306. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js +11 -0
  307. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js.map +7 -0
  308. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +253 -0
  309. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +7 -0
  310. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js +18 -0
  311. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js.map +7 -0
  312. package/dist/modules/warranty_claims/index.js +15 -0
  313. package/dist/modules/warranty_claims/index.js.map +7 -0
  314. package/dist/modules/warranty_claims/lib/aiAssist.js +496 -0
  315. package/dist/modules/warranty_claims/lib/aiAssist.js.map +7 -0
  316. package/dist/modules/warranty_claims/lib/apiIdsFilter.js +11 -0
  317. package/dist/modules/warranty_claims/lib/apiIdsFilter.js.map +7 -0
  318. package/dist/modules/warranty_claims/lib/assigneeNames.js +153 -0
  319. package/dist/modules/warranty_claims/lib/assigneeNames.js.map +7 -0
  320. package/dist/modules/warranty_claims/lib/attachmentVisibility.js +9 -0
  321. package/dist/modules/warranty_claims/lib/attachmentVisibility.js.map +7 -0
  322. package/dist/modules/warranty_claims/lib/bulkFeedback.js +8 -0
  323. package/dist/modules/warranty_claims/lib/bulkFeedback.js.map +7 -0
  324. package/dist/modules/warranty_claims/lib/businessHours.js +250 -0
  325. package/dist/modules/warranty_claims/lib/businessHours.js.map +7 -0
  326. package/dist/modules/warranty_claims/lib/businessHoursForm.js +146 -0
  327. package/dist/modules/warranty_claims/lib/businessHoursForm.js.map +7 -0
  328. package/dist/modules/warranty_claims/lib/claimTypeConfig.js +39 -0
  329. package/dist/modules/warranty_claims/lib/claimTypeConfig.js.map +7 -0
  330. package/dist/modules/warranty_claims/lib/decryptionSafety.js +17 -0
  331. package/dist/modules/warranty_claims/lib/decryptionSafety.js.map +7 -0
  332. package/dist/modules/warranty_claims/lib/deskFilters.js +88 -0
  333. package/dist/modules/warranty_claims/lib/deskFilters.js.map +7 -0
  334. package/dist/modules/warranty_claims/lib/dictionaries.js +157 -0
  335. package/dist/modules/warranty_claims/lib/dictionaries.js.map +7 -0
  336. package/dist/modules/warranty_claims/lib/dictionaryLabels.js +12 -0
  337. package/dist/modules/warranty_claims/lib/dictionaryLabels.js.map +7 -0
  338. package/dist/modules/warranty_claims/lib/emailIntake.js +87 -0
  339. package/dist/modules/warranty_claims/lib/emailIntake.js.map +7 -0
  340. package/dist/modules/warranty_claims/lib/escalation.js +58 -0
  341. package/dist/modules/warranty_claims/lib/escalation.js.map +7 -0
  342. package/dist/modules/warranty_claims/lib/externalIntake.js +233 -0
  343. package/dist/modules/warranty_claims/lib/externalIntake.js.map +7 -0
  344. package/dist/modules/warranty_claims/lib/grading.js +21 -0
  345. package/dist/modules/warranty_claims/lib/grading.js.map +7 -0
  346. package/dist/modules/warranty_claims/lib/listSegments.js +18 -0
  347. package/dist/modules/warranty_claims/lib/listSegments.js.map +7 -0
  348. package/dist/modules/warranty_claims/lib/orderOwnership.js +25 -0
  349. package/dist/modules/warranty_claims/lib/orderOwnership.js.map +7 -0
  350. package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js +11 -0
  351. package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js.map +7 -0
  352. package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js +108 -0
  353. package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js.map +7 -0
  354. package/dist/modules/warranty_claims/lib/portalClaimAccess.js +26 -0
  355. package/dist/modules/warranty_claims/lib/portalClaimAccess.js.map +7 -0
  356. package/dist/modules/warranty_claims/lib/quantity.js +19 -0
  357. package/dist/modules/warranty_claims/lib/quantity.js.map +7 -0
  358. package/dist/modules/warranty_claims/lib/risk.js +204 -0
  359. package/dist/modules/warranty_claims/lib/risk.js.map +7 -0
  360. package/dist/modules/warranty_claims/lib/settings.js +55 -0
  361. package/dist/modules/warranty_claims/lib/settings.js.map +7 -0
  362. package/dist/modules/warranty_claims/lib/stateMachine.js +88 -0
  363. package/dist/modules/warranty_claims/lib/stateMachine.js.map +7 -0
  364. package/dist/modules/warranty_claims/lib/timelineFilters.js +16 -0
  365. package/dist/modules/warranty_claims/lib/timelineFilters.js.map +7 -0
  366. package/dist/modules/warranty_claims/lib/triage.js +241 -0
  367. package/dist/modules/warranty_claims/lib/triage.js.map +7 -0
  368. package/dist/modules/warranty_claims/lib/troubleshooting.js +111 -0
  369. package/dist/modules/warranty_claims/lib/troubleshooting.js.map +7 -0
  370. package/dist/modules/warranty_claims/lib/tryResolve.js +11 -0
  371. package/dist/modules/warranty_claims/lib/tryResolve.js.map +7 -0
  372. package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js +101 -0
  373. package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js.map +7 -0
  374. package/dist/modules/warranty_claims/lib/warrantyPreview.js +18 -0
  375. package/dist/modules/warranty_claims/lib/warrantyPreview.js.map +7 -0
  376. package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js +32 -0
  377. package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js.map +7 -0
  378. package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js +16 -0
  379. package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js.map +7 -0
  380. package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js +17 -0
  381. package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js.map +7 -0
  382. package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js +29 -0
  383. package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js.map +7 -0
  384. package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js +25 -0
  385. package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js.map +7 -0
  386. package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js +13 -0
  387. package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js.map +7 -0
  388. package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js +15 -0
  389. package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js.map +7 -0
  390. package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js +13 -0
  391. package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js.map +7 -0
  392. package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js +13 -0
  393. package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js.map +7 -0
  394. package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js +12 -0
  395. package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js.map +7 -0
  396. package/dist/modules/warranty_claims/notifications.client.js +67 -0
  397. package/dist/modules/warranty_claims/notifications.client.js.map +7 -0
  398. package/dist/modules/warranty_claims/notifications.js +103 -0
  399. package/dist/modules/warranty_claims/notifications.js.map +7 -0
  400. package/dist/modules/warranty_claims/search.js +174 -0
  401. package/dist/modules/warranty_claims/search.js.map +7 -0
  402. package/dist/modules/warranty_claims/services/adjudicationEvaluator.js +202 -0
  403. package/dist/modules/warranty_claims/services/adjudicationEvaluator.js.map +7 -0
  404. package/dist/modules/warranty_claims/services/claimNumberGenerator.js +58 -0
  405. package/dist/modules/warranty_claims/services/claimNumberGenerator.js.map +7 -0
  406. package/dist/modules/warranty_claims/services/entitlementResolver.js +69 -0
  407. package/dist/modules/warranty_claims/services/entitlementResolver.js.map +7 -0
  408. package/dist/modules/warranty_claims/services/returnLabelProvider.js +14 -0
  409. package/dist/modules/warranty_claims/services/returnLabelProvider.js.map +7 -0
  410. package/dist/modules/warranty_claims/setup.js +121 -0
  411. package/dist/modules/warranty_claims/setup.js.map +7 -0
  412. package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js +119 -0
  413. package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js.map +7 -0
  414. package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js +47 -0
  415. package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js.map +7 -0
  416. package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js +60 -0
  417. package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js.map +7 -0
  418. package/dist/modules/warranty_claims/subscribers/claim-status-notification.js +83 -0
  419. package/dist/modules/warranty_claims/subscribers/claim-status-notification.js.map +7 -0
  420. package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js +46 -0
  421. package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js.map +7 -0
  422. package/dist/modules/warranty_claims/subscribers/email-to-claim.js +135 -0
  423. package/dist/modules/warranty_claims/subscribers/email-to-claim.js.map +7 -0
  424. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +164 -0
  425. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +7 -0
  426. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js +24 -0
  427. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js.map +7 -0
  428. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js +26 -0
  429. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js.map +7 -0
  430. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js +15 -0
  431. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js.map +7 -0
  432. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js +197 -0
  433. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js.map +7 -0
  434. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js +28 -0
  435. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js.map +7 -0
  436. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js +418 -0
  437. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js.map +7 -0
  438. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js +18 -0
  439. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js.map +7 -0
  440. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js +17 -0
  441. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js.map +7 -0
  442. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js +146 -0
  443. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js.map +7 -0
  444. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js +17 -0
  445. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js.map +7 -0
  446. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js +147 -0
  447. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js.map +7 -0
  448. package/dist/modules/warranty_claims/widgets/injection-table.js +38 -0
  449. package/dist/modules/warranty_claims/widgets/injection-table.js.map +7 -0
  450. package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js +223 -0
  451. package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js.map +7 -0
  452. package/dist/modules/warranty_claims/widgets/notifications/index.js +5 -0
  453. package/dist/modules/warranty_claims/widgets/notifications/index.js.map +7 -0
  454. package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js +334 -0
  455. package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js.map +7 -0
  456. package/generated/entities/sync_run/index.ts +1 -0
  457. package/generated/entities/warranty_claim/index.ts +51 -0
  458. package/generated/entities/warranty_claim_event/index.ts +11 -0
  459. package/generated/entities/warranty_claim_line/index.ts +37 -0
  460. package/generated/entities/warranty_claim_registration/index.ts +20 -0
  461. package/generated/entities/warranty_claim_sequence/index.ts +7 -0
  462. package/generated/entities/warranty_claim_settings/index.ts +20 -0
  463. package/generated/entities/warranty_claim_sla_signal/index.ts +11 -0
  464. package/generated/entities/warranty_troubleshooting_guide/index.ts +11 -0
  465. package/generated/entities/warranty_vendor_policy/index.ts +14 -0
  466. package/generated/entities.ids.generated.ts +12 -0
  467. package/generated/entity-fields-registry.ts +201 -0
  468. package/package.json +7 -7
  469. package/src/helpers/integration/api.ts +10 -3
  470. package/src/helpers/integration/salesFixtures.ts +17 -7
  471. package/src/modules/attachments/di.ts +30 -1
  472. package/src/modules/attachments/fields/attachment.tsx +215 -23
  473. package/src/modules/attachments/i18n/de.json +5 -0
  474. package/src/modules/attachments/i18n/en.json +5 -0
  475. package/src/modules/attachments/i18n/es.json +5 -0
  476. package/src/modules/attachments/i18n/ko.json +5 -0
  477. package/src/modules/attachments/i18n/pl.json +5 -0
  478. package/src/modules/attachments/lib/scoped-upload-service.ts +257 -0
  479. package/src/modules/attachments/lib/target-access-service.ts +57 -0
  480. package/src/modules/auth/api/sidebar/preferences/route.ts +2 -2
  481. package/src/modules/auth/lib/backendChrome.tsx +2 -2
  482. package/src/modules/auth/services/sidebarPreferencesService.ts +22 -3
  483. package/src/modules/customers/api/companies/route.ts +1 -0
  484. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +4 -2
  485. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +4 -2
  486. package/src/modules/customers/components/detail/assignableStaff.ts +14 -177
  487. package/src/modules/customers/lib/assignableStaff.ts +177 -0
  488. package/src/modules/data_sync/AGENTS.md +86 -8
  489. package/src/modules/data_sync/api/options.ts +1 -0
  490. package/src/modules/data_sync/api/run.ts +17 -0
  491. package/src/modules/data_sync/api/runs/[id]/retry.ts +32 -1
  492. package/src/modules/data_sync/api/runs/[id]/route.ts +1 -0
  493. package/src/modules/data_sync/backend/data-sync/page.tsx +73 -17
  494. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +62 -2
  495. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +28 -15
  496. package/src/modules/data_sync/components/RunParameterFields.tsx +229 -0
  497. package/src/modules/data_sync/data/entities.ts +4 -1
  498. package/src/modules/data_sync/data/validators.ts +3 -0
  499. package/src/modules/data_sync/i18n/de.json +10 -0
  500. package/src/modules/data_sync/i18n/en.json +10 -0
  501. package/src/modules/data_sync/i18n/es.json +10 -0
  502. package/src/modules/data_sync/i18n/ko.json +10 -0
  503. package/src/modules/data_sync/i18n/pl.json +10 -0
  504. package/src/modules/data_sync/lib/adapter.ts +88 -0
  505. package/src/modules/data_sync/lib/batch-stream.ts +117 -0
  506. package/src/modules/data_sync/lib/run-parameters.ts +196 -0
  507. package/src/modules/data_sync/lib/start-run.ts +2 -0
  508. package/src/modules/data_sync/lib/sync-engine.ts +193 -114
  509. package/src/modules/data_sync/lib/sync-run-service.ts +2 -0
  510. package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +17 -1
  511. package/src/modules/data_sync/migrations/Migration20260810120000.ts +13 -0
  512. package/src/modules/data_sync/workers/sync-scheduled.ts +32 -1
  513. package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +4 -0
  514. package/src/modules/inbox_ops/api/extract/route.ts +4 -0
  515. package/src/modules/inbox_ops/api/webhook/inbound.ts +4 -0
  516. package/src/modules/query_index/di.ts +153 -56
  517. package/src/modules/sales/commands/documents.ts +56 -12
  518. package/src/modules/sales/i18n/de.json +1 -0
  519. package/src/modules/sales/i18n/en.json +1 -0
  520. package/src/modules/sales/i18n/es.json +1 -0
  521. package/src/modules/sales/i18n/ko.json +1 -0
  522. package/src/modules/sales/i18n/pl.json +1 -0
  523. package/src/modules/warranty_claims/AGENTS.md +49 -0
  524. package/src/modules/warranty_claims/acl.ts +71 -0
  525. package/src/modules/warranty_claims/ai-agents.ts +189 -0
  526. package/src/modules/warranty_claims/ai-tools.ts +711 -0
  527. package/src/modules/warranty_claims/api/ai/assess/route.ts +371 -0
  528. package/src/modules/warranty_claims/api/ai/draft-reply/route.ts +149 -0
  529. package/src/modules/warranty_claims/api/ai/suggest/route.ts +190 -0
  530. package/src/modules/warranty_claims/api/assign/route.ts +142 -0
  531. package/src/modules/warranty_claims/api/assignees/route.ts +102 -0
  532. package/src/modules/warranty_claims/api/credit-memo/route.ts +191 -0
  533. package/src/modules/warranty_claims/api/entitlement/route.ts +224 -0
  534. package/src/modules/warranty_claims/api/events/route.ts +226 -0
  535. package/src/modules/warranty_claims/api/external/claims/route.ts +528 -0
  536. package/src/modules/warranty_claims/api/lines/route.ts +275 -0
  537. package/src/modules/warranty_claims/api/openapi.ts +28 -0
  538. package/src/modules/warranty_claims/api/portal/attachments/route.ts +547 -0
  539. package/src/modules/warranty_claims/api/portal/claims/[id]/route.ts +195 -0
  540. package/src/modules/warranty_claims/api/portal/claims/[id]/shared.ts +125 -0
  541. package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +82 -0
  542. package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +87 -0
  543. package/src/modules/warranty_claims/api/portal/claims/route.ts +579 -0
  544. package/src/modules/warranty_claims/api/portal/events/route.ts +260 -0
  545. package/src/modules/warranty_claims/api/portal/options/route.ts +113 -0
  546. package/src/modules/warranty_claims/api/portal/orders/lines/route.ts +258 -0
  547. package/src/modules/warranty_claims/api/portal/orders/route.ts +206 -0
  548. package/src/modules/warranty_claims/api/portal/troubleshooting/route.ts +151 -0
  549. package/src/modules/warranty_claims/api/receiving/route.ts +215 -0
  550. package/src/modules/warranty_claims/api/registrations/route.ts +417 -0
  551. package/src/modules/warranty_claims/api/replacement-order/route.ts +188 -0
  552. package/src/modules/warranty_claims/api/return-label/route.ts +291 -0
  553. package/src/modules/warranty_claims/api/risk/route.ts +112 -0
  554. package/src/modules/warranty_claims/api/route.ts +543 -0
  555. package/src/modules/warranty_claims/api/sales-return/route.ts +183 -0
  556. package/src/modules/warranty_claims/api/settings-general/route.ts +230 -0
  557. package/src/modules/warranty_claims/api/stats/route.ts +306 -0
  558. package/src/modules/warranty_claims/api/submit/route.ts +146 -0
  559. package/src/modules/warranty_claims/api/transition/route.ts +144 -0
  560. package/src/modules/warranty_claims/api/troubleshooting-guides/route.ts +295 -0
  561. package/src/modules/warranty_claims/api/vendor-policies/route.ts +351 -0
  562. package/src/modules/warranty_claims/api/vendor-recovery/route.ts +148 -0
  563. package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +167 -0
  564. package/src/modules/warranty_claims/backend/components/AiAssessButtons.tsx +389 -0
  565. package/src/modules/warranty_claims/backend/components/ClaimStageProgress.tsx +65 -0
  566. package/src/modules/warranty_claims/backend/components/ClaimStatusBadge.tsx +137 -0
  567. package/src/modules/warranty_claims/backend/components/ClaimsKpiStrip.tsx +169 -0
  568. package/src/modules/warranty_claims/backend/components/EntitlementLookupBadge.tsx +203 -0
  569. package/src/modules/warranty_claims/backend/components/ReceivingPanel.tsx +300 -0
  570. package/src/modules/warranty_claims/backend/components/ReturnLabelPanel.tsx +292 -0
  571. package/src/modules/warranty_claims/backend/components/TroubleshootingWalker.tsx +125 -0
  572. package/src/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.tsx +217 -0
  573. package/src/modules/warranty_claims/backend/components/WarrantyWorkspace.tsx +66 -0
  574. package/src/modules/warranty_claims/backend/components/claimReasonOptions.ts +57 -0
  575. package/src/modules/warranty_claims/backend/components/claimSla.tsx +127 -0
  576. package/src/modules/warranty_claims/backend/components/orderLookup.ts +109 -0
  577. package/src/modules/warranty_claims/backend/components/productLookup.tsx +433 -0
  578. package/src/modules/warranty_claims/backend/components/useUserDisplayNames.ts +66 -0
  579. package/src/modules/warranty_claims/backend/page.meta.ts +12 -0
  580. package/src/modules/warranty_claims/backend/page.tsx +1302 -0
  581. package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.ts +12 -0
  582. package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.tsx +531 -0
  583. package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.ts +12 -0
  584. package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +2599 -0
  585. package/src/modules/warranty_claims/backend/warranty_claims/create/page.meta.ts +12 -0
  586. package/src/modules/warranty_claims/backend/warranty_claims/create/page.tsx +1533 -0
  587. package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.ts +13 -0
  588. package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.tsx +136 -0
  589. package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.ts +13 -0
  590. package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.tsx +52 -0
  591. package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.ts +15 -0
  592. package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.tsx +555 -0
  593. package/src/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.tsx +295 -0
  594. package/src/modules/warranty_claims/backend/warranty_claims/settings/page.meta.ts +15 -0
  595. package/src/modules/warranty_claims/backend/warranty_claims/settings/page.tsx +1903 -0
  596. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.tsx +550 -0
  597. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.ts +15 -0
  598. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.tsx +141 -0
  599. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.ts +15 -0
  600. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.tsx +57 -0
  601. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.ts +17 -0
  602. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.tsx +477 -0
  603. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.tsx +214 -0
  604. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.ts +15 -0
  605. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.tsx +137 -0
  606. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.ts +15 -0
  607. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.tsx +51 -0
  608. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.ts +17 -0
  609. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.tsx +506 -0
  610. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.tsx +220 -0
  611. package/src/modules/warranty_claims/ce.ts +44 -0
  612. package/src/modules/warranty_claims/commands/claim-lines.ts +1082 -0
  613. package/src/modules/warranty_claims/commands/claims.ts +3364 -0
  614. package/src/modules/warranty_claims/commands/index.ts +3 -0
  615. package/src/modules/warranty_claims/commands/settings.ts +198 -0
  616. package/src/modules/warranty_claims/commands/shared.ts +212 -0
  617. package/src/modules/warranty_claims/data/constants.ts +37 -0
  618. package/src/modules/warranty_claims/data/enrichers.ts +165 -0
  619. package/src/modules/warranty_claims/data/entities.ts +663 -0
  620. package/src/modules/warranty_claims/data/validators.ts +634 -0
  621. package/src/modules/warranty_claims/di.ts +34 -0
  622. package/src/modules/warranty_claims/encryption.ts +26 -0
  623. package/src/modules/warranty_claims/events.ts +81 -0
  624. package/src/modules/warranty_claims/extension-points.ts +16 -0
  625. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.ts +9 -0
  626. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +1037 -0
  627. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.ts +9 -0
  628. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.tsx +1409 -0
  629. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.ts +16 -0
  630. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +304 -0
  631. package/src/modules/warranty_claims/i18n/de.json +1270 -0
  632. package/src/modules/warranty_claims/i18n/en.json +1270 -0
  633. package/src/modules/warranty_claims/i18n/es.json +1270 -0
  634. package/src/modules/warranty_claims/i18n/ko.json +1270 -0
  635. package/src/modules/warranty_claims/i18n/pl.json +1270 -0
  636. package/src/modules/warranty_claims/index.ts +13 -0
  637. package/src/modules/warranty_claims/lib/aiAssist.ts +677 -0
  638. package/src/modules/warranty_claims/lib/apiIdsFilter.ts +12 -0
  639. package/src/modules/warranty_claims/lib/assigneeNames.ts +184 -0
  640. package/src/modules/warranty_claims/lib/attachmentVisibility.ts +5 -0
  641. package/src/modules/warranty_claims/lib/bulkFeedback.ts +6 -0
  642. package/src/modules/warranty_claims/lib/businessHours.ts +333 -0
  643. package/src/modules/warranty_claims/lib/businessHoursForm.ts +206 -0
  644. package/src/modules/warranty_claims/lib/claimTypeConfig.ts +49 -0
  645. package/src/modules/warranty_claims/lib/decryptionSafety.ts +19 -0
  646. package/src/modules/warranty_claims/lib/deskFilters.ts +131 -0
  647. package/src/modules/warranty_claims/lib/dictionaries.ts +212 -0
  648. package/src/modules/warranty_claims/lib/dictionaryLabels.ts +16 -0
  649. package/src/modules/warranty_claims/lib/emailIntake.ts +123 -0
  650. package/src/modules/warranty_claims/lib/escalation.ts +83 -0
  651. package/src/modules/warranty_claims/lib/externalIntake.ts +362 -0
  652. package/src/modules/warranty_claims/lib/grading.ts +23 -0
  653. package/src/modules/warranty_claims/lib/listSegments.ts +19 -0
  654. package/src/modules/warranty_claims/lib/orderOwnership.ts +58 -0
  655. package/src/modules/warranty_claims/lib/portalAttachmentUpload.ts +11 -0
  656. package/src/modules/warranty_claims/lib/portalAttachmentValidation.ts +115 -0
  657. package/src/modules/warranty_claims/lib/portalClaimAccess.ts +37 -0
  658. package/src/modules/warranty_claims/lib/quantity.ts +15 -0
  659. package/src/modules/warranty_claims/lib/risk.ts +371 -0
  660. package/src/modules/warranty_claims/lib/settings.ts +76 -0
  661. package/src/modules/warranty_claims/lib/stateMachine.ts +130 -0
  662. package/src/modules/warranty_claims/lib/timelineFilters.ts +22 -0
  663. package/src/modules/warranty_claims/lib/triage.ts +366 -0
  664. package/src/modules/warranty_claims/lib/troubleshooting.ts +157 -0
  665. package/src/modules/warranty_claims/lib/tryResolve.ts +7 -0
  666. package/src/modules/warranty_claims/lib/vendorPolicyRecovery.ts +182 -0
  667. package/src/modules/warranty_claims/lib/warrantyPreview.ts +20 -0
  668. package/src/modules/warranty_claims/migrations/.snapshot-open-mercato.json +3525 -0
  669. package/src/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.ts +38 -0
  670. package/src/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.ts +17 -0
  671. package/src/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.ts +17 -0
  672. package/src/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.ts +37 -0
  673. package/src/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.ts +25 -0
  674. package/src/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.ts +13 -0
  675. package/src/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.ts +15 -0
  676. package/src/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.ts +13 -0
  677. package/src/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.ts +13 -0
  678. package/src/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.ts +11 -0
  679. package/src/modules/warranty_claims/notifications.client.ts +78 -0
  680. package/src/modules/warranty_claims/notifications.ts +101 -0
  681. package/src/modules/warranty_claims/search.ts +199 -0
  682. package/src/modules/warranty_claims/services/adjudicationEvaluator.ts +344 -0
  683. package/src/modules/warranty_claims/services/claimNumberGenerator.ts +69 -0
  684. package/src/modules/warranty_claims/services/entitlementResolver.ts +102 -0
  685. package/src/modules/warranty_claims/services/returnLabelProvider.ts +30 -0
  686. package/src/modules/warranty_claims/setup.ts +154 -0
  687. package/src/modules/warranty_claims/subscribers/auto-vendor-recovery.ts +140 -0
  688. package/src/modules/warranty_claims/subscribers/claim-assigned-notification.ts +54 -0
  689. package/src/modules/warranty_claims/subscribers/claim-customer-reply-notification.ts +68 -0
  690. package/src/modules/warranty_claims/subscribers/claim-status-notification.ts +100 -0
  691. package/src/modules/warranty_claims/subscribers/claim-submitted-notification.ts +53 -0
  692. package/src/modules/warranty_claims/subscribers/email-to-claim.ts +175 -0
  693. package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +226 -0
  694. package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.ts +28 -0
  695. package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.ts +31 -0
  696. package/src/modules/warranty_claims/widgets/dashboard/claims-queue/config.ts +15 -0
  697. package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.tsx +237 -0
  698. package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.ts +27 -0
  699. package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.tsx +527 -0
  700. package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.ts +37 -0
  701. package/src/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.ts +21 -0
  702. package/src/modules/warranty_claims/widgets/injection/customer-claims-tab.tsx +210 -0
  703. package/src/modules/warranty_claims/widgets/injection/order-claims-tab/widget.ts +21 -0
  704. package/src/modules/warranty_claims/widgets/injection/order-claims-tab.tsx +210 -0
  705. package/src/modules/warranty_claims/widgets/injection-table.ts +36 -0
  706. package/src/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.tsx +253 -0
  707. package/src/modules/warranty_claims/widgets/notifications/index.ts +1 -0
  708. package/src/modules/warranty_claims/workers/sla-escalation-sweep.ts +450 -0
@@ -0,0 +1,202 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
3
+ import { createLogger } from "@open-mercato/shared/lib/logger";
4
+ import { Attachment, AttachmentPartition } from "../data/entities.js";
5
+ import { attachmentCrudEvents, attachmentCrudIndexer } from "./crud.js";
6
+ import { buildAttachmentFileUrl } from "./imageUrls.js";
7
+ import { ensureDefaultPartitions, resolveDefaultPartitionCode } from "./partitions.js";
8
+ import { extractAttachmentContent } from "./textExtraction.js";
9
+ import { requestOcrProcessing } from "./ocrQueue.js";
10
+ import { OcrService, shouldUseLlmOcr } from "./ocrService.js";
11
+ import { assertAttachmentScopeInvariant } from "./access.js";
12
+ import {
13
+ mergeAttachmentMetadata,
14
+ upsertAssignment
15
+ } from "./metadata.js";
16
+ import {
17
+ detectAttachmentMimeType,
18
+ hasDangerousExecutableExtension,
19
+ isActiveContentAttachment,
20
+ sanitizeUploadedFileName
21
+ } from "./security.js";
22
+ import { resolveAttachmentMaxBytes } from "./upload-limits.js";
23
+ const logger = createLogger("attachments");
24
+ class ScopedAttachmentUploadError extends Error {
25
+ constructor(code, status) {
26
+ super(code);
27
+ this.code = code;
28
+ this.status = status;
29
+ this.name = "ScopedAttachmentUploadError";
30
+ }
31
+ }
32
+ class ScopedAttachmentUploadService {
33
+ constructor(deps) {
34
+ this.deps = deps;
35
+ }
36
+ async upload(input) {
37
+ const safeName = sanitizeUploadedFileName(input.fileName);
38
+ if (hasDangerousExecutableExtension(safeName)) {
39
+ throw new ScopedAttachmentUploadError("dangerous_executable", 400);
40
+ }
41
+ if (input.buffer.length > (input.maxBytes ?? resolveAttachmentMaxBytes())) {
42
+ throw new ScopedAttachmentUploadError("max_upload_size", 413);
43
+ }
44
+ const mimeType = detectAttachmentMimeType(input.buffer, safeName, input.declaredMimeType ?? "");
45
+ if (isActiveContentAttachment(input.buffer, safeName, mimeType)) {
46
+ throw new ScopedAttachmentUploadError("active_content", 400);
47
+ }
48
+ const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps;
49
+ await ensureDefaultPartitions(em);
50
+ const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId);
51
+ const partition = await em.findOne(AttachmentPartition, {
52
+ code: partitionCode,
53
+ $or: [
54
+ { tenantId: null, organizationId: null },
55
+ { tenantId: input.tenantId, organizationId: input.organizationId }
56
+ ]
57
+ });
58
+ if (!partition) throw new ScopedAttachmentUploadError("partition_unavailable", 400);
59
+ const driver = await storageDriverFactory.resolveForPartition(partition.code, {
60
+ tenantId: input.tenantId,
61
+ organizationId: input.organizationId
62
+ });
63
+ const preparedStoragePath = driver.prepareStoragePath?.({
64
+ partitionCode: partition.code,
65
+ orgId: input.organizationId,
66
+ tenantId: input.tenantId,
67
+ fileName: safeName
68
+ });
69
+ if (!preparedStoragePath) {
70
+ throw new ScopedAttachmentUploadError("quota_recovery_unsupported", 500);
71
+ }
72
+ let reservation = null;
73
+ try {
74
+ reservation = await attachmentQuotaService.reserve({
75
+ tenantId: input.tenantId,
76
+ organizationId: input.organizationId,
77
+ bytes: input.buffer.length,
78
+ source: "attachment",
79
+ storageDriver: driver.key,
80
+ storagePath: preparedStoragePath,
81
+ partitionCode: partition.code
82
+ });
83
+ await attachmentQuotaRecoveryScheduler({
84
+ reservationId: reservation.id,
85
+ tenantId: input.tenantId,
86
+ organizationId: input.organizationId
87
+ }, Math.max(1e3, reservation.expiresAt.getTime() - Date.now()));
88
+ await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken);
89
+ } catch (error) {
90
+ if (reservation) {
91
+ await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => void 0);
92
+ }
93
+ const code = error?.code;
94
+ if (code === "quota_exceeded") throw new ScopedAttachmentUploadError("quota_exceeded", 413);
95
+ if (code === "quota_target_exists") throw new ScopedAttachmentUploadError("quota_target_exists", 409);
96
+ throw new ScopedAttachmentUploadError("quota_unavailable", 500);
97
+ }
98
+ if (!reservation) throw new ScopedAttachmentUploadError("quota_unavailable", 500);
99
+ let storedPath = preparedStoragePath;
100
+ try {
101
+ const stored = await driver.store({
102
+ partitionCode: partition.code,
103
+ orgId: input.organizationId,
104
+ tenantId: input.tenantId,
105
+ fileName: safeName,
106
+ buffer: input.buffer,
107
+ storagePath: preparedStoragePath
108
+ });
109
+ storedPath = stored.storagePath;
110
+ await attachmentQuotaService.markStored(reservation.id, reservation.leaseToken);
111
+ } catch (error) {
112
+ await this.compensateStorage(driver, partition.code, storedPath, reservation);
113
+ logger.error("Scoped attachment storage failed", { err: error });
114
+ throw new ScopedAttachmentUploadError("storage_failed", 500);
115
+ }
116
+ let extractedContent = null;
117
+ const wantsLlmOcr = partition.requiresOcr && shouldUseLlmOcr(mimeType, safeName);
118
+ const ocrService = wantsLlmOcr ? new OcrService() : null;
119
+ const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available);
120
+ if (partition.requiresOcr && !useLlmOcr) {
121
+ try {
122
+ const { filePath, cleanup } = await driver.toLocalPath(partition.code, storedPath);
123
+ try {
124
+ extractedContent = await extractAttachmentContent({ filePath, mimeType });
125
+ } finally {
126
+ await cleanup().catch(() => void 0);
127
+ }
128
+ } catch (error) {
129
+ logger.error("Scoped attachment text extraction failed", { err: error });
130
+ }
131
+ }
132
+ let assignments = input.assignments?.slice() ?? [];
133
+ assignments = upsertAssignment(assignments, { type: input.entityId, id: input.recordId });
134
+ const metadata = mergeAttachmentMetadata(null, {
135
+ assignments,
136
+ tags: input.tags ?? []
137
+ });
138
+ const attachmentId = randomUUID();
139
+ let attachment;
140
+ try {
141
+ await em.transactional(async (tx) => {
142
+ attachment = tx.create(Attachment, {
143
+ id: attachmentId,
144
+ entityId: input.entityId,
145
+ recordId: input.recordId,
146
+ organizationId: input.organizationId,
147
+ tenantId: input.tenantId,
148
+ fileName: safeName,
149
+ mimeType,
150
+ fileSize: input.buffer.length,
151
+ partitionCode: partition.code,
152
+ storageDriver: partition.storageDriver || "local",
153
+ storagePath: storedPath,
154
+ url: buildAttachmentFileUrl(attachmentId),
155
+ content: extractedContent,
156
+ storageMetadata: metadata
157
+ });
158
+ assertAttachmentScopeInvariant({ tenantId: attachment.tenantId, organizationId: attachment.organizationId });
159
+ await tx.persist(attachment).flush();
160
+ await attachmentQuotaService.completeAttachment(reservation.id, reservation.leaseToken, tx);
161
+ });
162
+ } catch (error) {
163
+ await this.compensateStorage(driver, partition.code, storedPath, reservation);
164
+ logger.error("Scoped attachment persistence failed", { err: error });
165
+ throw new ScopedAttachmentUploadError("persistence_failed", 500);
166
+ }
167
+ if (useLlmOcr) {
168
+ requestOcrProcessing(em, attachment, driver, storedPath).catch((error) => {
169
+ logger.error("Scoped attachment OCR scheduling failed", { err: error });
170
+ });
171
+ }
172
+ if (this.deps.dataEngine) {
173
+ await emitCrudSideEffects({
174
+ dataEngine: this.deps.dataEngine,
175
+ action: "created",
176
+ entity: attachment,
177
+ identifiers: {
178
+ id: attachment.id,
179
+ organizationId: attachment.organizationId ?? null,
180
+ tenantId: attachment.tenantId ?? null
181
+ },
182
+ events: attachmentCrudEvents,
183
+ indexer: attachmentCrudIndexer
184
+ });
185
+ await this.deps.dataEngine.flushOrmEntityChanges();
186
+ }
187
+ return attachment;
188
+ }
189
+ async compensateStorage(driver, partitionCode, storagePath, reservation) {
190
+ try {
191
+ await (driver.deleteStrict?.(partitionCode, storagePath) ?? driver.delete(partitionCode, storagePath));
192
+ await this.deps.attachmentQuotaService.release(reservation.id, reservation.leaseToken);
193
+ } catch (error) {
194
+ logger.error("Scoped attachment compensation failed", { err: error });
195
+ }
196
+ }
197
+ }
198
+ export {
199
+ ScopedAttachmentUploadError,
200
+ ScopedAttachmentUploadService
201
+ };
202
+ //# sourceMappingURL=scoped-upload-service.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/attachments/lib/scoped-upload-service.ts"],
4
+ "sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from './crud'\nimport type { StorageDriverFactory } from './drivers'\nimport { buildAttachmentFileUrl } from './imageUrls'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'\nimport { extractAttachmentContent } from './textExtraction'\nimport { requestOcrProcessing } from './ocrQueue'\nimport { OcrService, shouldUseLlmOcr } from './ocrService'\nimport { assertAttachmentScopeInvariant } from './access'\nimport {\n mergeAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from './metadata'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from './security'\nimport { resolveAttachmentMaxBytes } from './upload-limits'\nimport type { AttachmentQuotaService } from './quota-service'\n\nconst logger = createLogger('attachments')\n\nexport type ScopedAttachmentUploadErrorCode =\n | 'dangerous_executable'\n | 'max_upload_size'\n | 'active_content'\n | 'partition_unavailable'\n | 'quota_exceeded'\n | 'quota_target_exists'\n | 'quota_unavailable'\n | 'quota_recovery_unsupported'\n | 'storage_failed'\n | 'persistence_failed'\n\nexport class ScopedAttachmentUploadError extends Error {\n constructor(\n public readonly code: ScopedAttachmentUploadErrorCode,\n public readonly status: number,\n ) {\n super(code)\n this.name = 'ScopedAttachmentUploadError'\n }\n}\n\nexport type ScopedAttachmentUploadInput = {\n tenantId: string\n organizationId: string\n entityId: string\n recordId: string\n fileName: string\n declaredMimeType?: string | null\n buffer: Buffer\n tags?: string[]\n assignments?: AttachmentAssignment[]\n partitionCode?: string | null\n maxBytes?: number\n}\n\ntype QuotaRecoveryScheduler = (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n) => Promise<void>\n\nexport class ScopedAttachmentUploadService {\n constructor(private readonly deps: {\n em: EntityManager\n dataEngine?: DataEngine | null\n storageDriverFactory: StorageDriverFactory\n attachmentQuotaService: AttachmentQuotaService\n attachmentQuotaRecoveryScheduler: QuotaRecoveryScheduler\n }) {}\n\n async upload(input: ScopedAttachmentUploadInput): Promise<Attachment> {\n const safeName = sanitizeUploadedFileName(input.fileName)\n if (hasDangerousExecutableExtension(safeName)) {\n throw new ScopedAttachmentUploadError('dangerous_executable', 400)\n }\n if (input.buffer.length > (input.maxBytes ?? resolveAttachmentMaxBytes())) {\n throw new ScopedAttachmentUploadError('max_upload_size', 413)\n }\n const mimeType = detectAttachmentMimeType(input.buffer, safeName, input.declaredMimeType ?? '')\n if (isActiveContentAttachment(input.buffer, safeName, mimeType)) {\n throw new ScopedAttachmentUploadError('active_content', 400)\n }\n\n const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps\n await ensureDefaultPartitions(em)\n const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId)\n const partition = await em.findOne(AttachmentPartition, {\n code: partitionCode,\n $or: [\n { tenantId: null, organizationId: null },\n { tenantId: input.tenantId, organizationId: input.organizationId },\n ],\n })\n if (!partition) throw new ScopedAttachmentUploadError('partition_unavailable', 400)\n\n const driver = await storageDriverFactory.resolveForPartition(partition.code, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const preparedStoragePath = driver.prepareStoragePath?.({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n })\n if (!preparedStoragePath) {\n throw new ScopedAttachmentUploadError('quota_recovery_unsupported', 500)\n }\n\n let reservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n try {\n reservation = await attachmentQuotaService.reserve({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n bytes: input.buffer.length,\n source: 'attachment',\n storageDriver: driver.key,\n storagePath: preparedStoragePath,\n partitionCode: partition.code,\n })\n await attachmentQuotaRecoveryScheduler({\n reservationId: reservation.id,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, Math.max(1_000, reservation.expiresAt.getTime() - Date.now()))\n await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken)\n } catch (error) {\n if (reservation) {\n await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => undefined)\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') throw new ScopedAttachmentUploadError('quota_exceeded', 413)\n if (code === 'quota_target_exists') throw new ScopedAttachmentUploadError('quota_target_exists', 409)\n throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n }\n\n if (!reservation) throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n\n let storedPath = preparedStoragePath\n try {\n const stored = await driver.store({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n buffer: input.buffer,\n storagePath: preparedStoragePath,\n })\n storedPath = stored.storagePath\n await attachmentQuotaService.markStored(reservation.id, reservation.leaseToken)\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment storage failed', { err: error })\n throw new ScopedAttachmentUploadError('storage_failed', 500)\n }\n\n let extractedContent: string | null = null\n const wantsLlmOcr = partition.requiresOcr && shouldUseLlmOcr(mimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n if (partition.requiresOcr && !useLlmOcr) {\n try {\n const { filePath, cleanup } = await driver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({ filePath, mimeType })\n } finally {\n await cleanup().catch(() => undefined)\n }\n } catch (error) {\n logger.error('Scoped attachment text extraction failed', { err: error })\n }\n }\n\n let assignments = input.assignments?.slice() ?? []\n assignments = upsertAssignment(assignments, { type: input.entityId, id: input.recordId })\n const metadata = mergeAttachmentMetadata(null, {\n assignments,\n tags: input.tags ?? [],\n })\n const attachmentId = randomUUID()\n let attachment!: Attachment\n\n try {\n await em.transactional(async (tx) => {\n attachment = tx.create(Attachment, {\n id: attachmentId,\n entityId: input.entityId,\n recordId: input.recordId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n mimeType,\n fileSize: input.buffer.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: storedPath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n assertAttachmentScopeInvariant({ tenantId: attachment.tenantId, organizationId: attachment.organizationId })\n await tx.persist(attachment).flush()\n await attachmentQuotaService.completeAttachment(reservation.id, reservation.leaseToken, tx)\n })\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment persistence failed', { err: error })\n throw new ScopedAttachmentUploadError('persistence_failed', 500)\n }\n\n if (useLlmOcr) {\n requestOcrProcessing(em, attachment, driver, storedPath).catch((error) => {\n logger.error('Scoped attachment OCR scheduling failed', { err: error })\n })\n }\n if (this.deps.dataEngine) {\n await emitCrudSideEffects({\n dataEngine: this.deps.dataEngine,\n action: 'created',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await this.deps.dataEngine.flushOrmEntityChanges()\n }\n return attachment\n }\n\n private async compensateStorage(\n driver: Awaited<ReturnType<StorageDriverFactory['resolveForPartition']>>,\n partitionCode: string,\n storagePath: string,\n reservation: { id: string; leaseToken: string },\n ): Promise<void> {\n try {\n await (driver.deleteStrict?.(partitionCode, storagePath) ?? driver.delete(partitionCode, storagePath))\n await this.deps.attachmentQuotaService.release(reservation.id, reservation.leaseToken)\n } catch (error) {\n logger.error('Scoped attachment compensation failed', { err: error })\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,39 @@
1
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
+ import { Attachment, AttachmentPartition } from "../data/entities.js";
3
+ import { checkAttachmentAccess } from "./access.js";
4
+ import { readAttachmentMetadata } from "./metadata.js";
5
+ class AttachmentTargetAccessService {
6
+ constructor(em) {
7
+ this.em = em;
8
+ }
9
+ async canAccessLinkedTarget(input) {
10
+ if (!input.targets.length) return false;
11
+ const attachment = await findOneWithDecryption(
12
+ this.em,
13
+ Attachment,
14
+ {
15
+ id: input.attachmentId,
16
+ tenantId: input.tenantId,
17
+ organizationId: input.organizationId
18
+ },
19
+ {},
20
+ { tenantId: input.tenantId, organizationId: input.organizationId }
21
+ );
22
+ if (!attachment) return false;
23
+ const partition = await this.em.findOne(AttachmentPartition, {
24
+ code: attachment.partitionCode,
25
+ $or: [
26
+ { tenantId: null, organizationId: null },
27
+ { tenantId: input.tenantId, organizationId: input.organizationId }
28
+ ]
29
+ });
30
+ if (!partition) return false;
31
+ if (!checkAttachmentAccess(input.auth, attachment, partition, { requireAuthForPublic: true }).ok) return false;
32
+ const metadata = readAttachmentMetadata(attachment.storageMetadata);
33
+ return input.targets.some((target) => attachment.entityId === target.entityId && attachment.recordId === target.recordId || metadata.assignments?.some((assignment) => assignment.type === target.entityId && assignment.id === target.recordId) === true);
34
+ }
35
+ }
36
+ export {
37
+ AttachmentTargetAccessService
38
+ };
39
+ //# sourceMappingURL=target-access-service.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/attachments/lib/target-access-service.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { checkAttachmentAccess } from './access'\nimport { readAttachmentMetadata } from './metadata'\n\nexport type AttachmentRecordTarget = {\n entityId: string\n recordId: string\n}\n\nexport type AttachmentTargetAccessInput = {\n attachmentId: string\n tenantId: string\n organizationId: string\n auth: Exclude<AuthContext, null>\n targets: AttachmentRecordTarget[]\n}\n\nexport class AttachmentTargetAccessService {\n constructor(private readonly em: EntityManager) {}\n\n async canAccessLinkedTarget(input: AttachmentTargetAccessInput): Promise<boolean> {\n if (!input.targets.length) return false\n const attachment = await findOneWithDecryption(\n this.em,\n Attachment,\n {\n id: input.attachmentId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n },\n {},\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n if (!attachment) return false\n\n const partition = await this.em.findOne(AttachmentPartition, {\n code: attachment.partitionCode,\n $or: [\n { tenantId: null, organizationId: null },\n { tenantId: input.tenantId, organizationId: input.organizationId },\n ],\n })\n if (!partition) return false\n if (!checkAttachmentAccess(input.auth, attachment, partition, { requireAuthForPublic: true }).ok) return false\n\n const metadata = readAttachmentMetadata(attachment.storageMetadata)\n return input.targets.some((target) => (\n (attachment.entityId === target.entityId && attachment.recordId === target.recordId)\n || metadata.assignments?.some((assignment) => (\n assignment.type === target.entityId && assignment.id === target.recordId\n )) === true\n ))\n }\n}\n"],
5
+ "mappings": "AAEA,SAAS,6BAA6B;AACtC,SAAS,YAAY,2BAA2B;AAChD,SAAS,6BAA6B;AACtC,SAAS,8BAA8B;AAehC,MAAM,8BAA8B;AAAA,EACzC,YAA6B,IAAmB;AAAnB;AAAA,EAAoB;AAAA,EAEjD,MAAM,sBAAsB,OAAsD;AAChF,QAAI,CAAC,MAAM,QAAQ,OAAQ,QAAO;AAClC,UAAM,aAAa,MAAM;AAAA,MACvB,KAAK;AAAA,MACL;AAAA,MACA;AAAA,QACE,IAAI,MAAM;AAAA,QACV,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,MACxB;AAAA,MACA,CAAC;AAAA,MACD,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,IACnE;AACA,QAAI,CAAC,WAAY,QAAO;AAExB,UAAM,YAAY,MAAM,KAAK,GAAG,QAAQ,qBAAqB;AAAA,MAC3D,MAAM,WAAW;AAAA,MACjB,KAAK;AAAA,QACH,EAAE,UAAU,MAAM,gBAAgB,KAAK;AAAA,QACvC,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,MACnE;AAAA,IACF,CAAC;AACD,QAAI,CAAC,UAAW,QAAO;AACvB,QAAI,CAAC,sBAAsB,MAAM,MAAM,YAAY,WAAW,EAAE,sBAAsB,KAAK,CAAC,EAAE,GAAI,QAAO;AAEzG,UAAM,WAAW,uBAAuB,WAAW,eAAe;AAClE,WAAO,MAAM,QAAQ,KAAK,CAAC,WACxB,WAAW,aAAa,OAAO,YAAY,WAAW,aAAa,OAAO,YACxE,SAAS,aAAa,KAAK,CAAC,eAC7B,WAAW,SAAS,OAAO,YAAY,WAAW,OAAO,OAAO,QACjE,MAAM,IACR;AAAA,EACH;AACF;",
6
+ "names": []
7
+ }
@@ -8,9 +8,9 @@ import {
8
8
  sidebarPreferencesScopeSchema
9
9
  } from "../../../data/validators.js";
10
10
  import {
11
+ findSidebarPreference,
11
12
  loadRoleSidebarPreferenceUpdatedAt,
12
13
  loadRoleSidebarPreferences,
13
- loadSidebarPreference,
14
14
  loadSidebarPreferenceUpdatedAt,
15
15
  saveRoleSidebarPreference,
16
16
  saveSidebarPreference
@@ -155,7 +155,7 @@ async function GET(req) {
155
155
  });
156
156
  }
157
157
  const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub;
158
- const settings = effectiveUserId ? await loadSidebarPreference(em, {
158
+ const settings = effectiveUserId ? await findSidebarPreference(em, {
159
159
  userId: effectiveUserId,
160
160
  tenantId: auth.tenantId ?? null,
161
161
  organizationId: auth.orgId ?? null,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/api/sidebar/preferences/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n sidebarPreferencesInputSchema,\n sidebarPreferencesScopeSchema,\n} from '../../../data/validators'\nimport {\n loadRoleSidebarPreferenceUpdatedAt,\n loadRoleSidebarPreferences,\n loadSidebarPreference,\n loadSidebarPreferenceUpdatedAt,\n saveRoleSidebarPreference,\n saveSidebarPreference,\n} from '../../../services/sidebarPreferencesService'\nimport { SIDEBAR_PREFERENCES_VERSION } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { Role, RoleSidebarPreference } from '../../../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { z } from 'zod'\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true, requireFeatures: ['auth.sidebar.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.sidebar.manage'] },\n}\n\nconst sidebarSettingsSchema = z.object({\n version: z.number().int().positive(),\n groupOrder: z.array(z.string()),\n groupLabels: z.record(z.string(), z.string()),\n itemLabels: z.record(z.string(), z.string()),\n hiddenItems: z.array(z.string()),\n itemOrder: z.record(z.string(), z.array(z.string())),\n})\n\nconst sidebarRoleEntrySchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n hasPreference: z.boolean(),\n})\n\nconst sidebarPreferencesResponseSchema = z.object({\n locale: z.string(),\n settings: sidebarSettingsSchema,\n canApplyToRoles: z.boolean(),\n roles: z.array(sidebarRoleEntrySchema),\n scope: sidebarPreferencesScopeSchema,\n updatedAt: z.string().datetime().nullable(),\n})\n\nconst sidebarPreferencesUpdateResponseSchema = sidebarPreferencesResponseSchema.extend({\n appliedRoles: z.array(z.string().uuid()),\n clearedRoles: z.array(z.string().uuid()),\n})\n\nconst sidebarPreferencesDeleteResponseSchema = z.object({\n ok: z.literal(true),\n scope: sidebarPreferencesScopeSchema,\n})\n\nconst sidebarErrorSchema = z.object({\n error: z.string(),\n})\n\nconst FEATURE_MANAGE = 'auth.sidebar.manage'\n\ntype EmptySettings = {\n version: number\n groupOrder: string[]\n groupLabels: Record<string, string>\n itemLabels: Record<string, string>\n hiddenItems: string[]\n itemOrder: Record<string, string[]>\n}\n\nfunction emptySettings(): EmptySettings {\n return {\n version: SIDEBAR_PREFERENCES_VERSION,\n groupOrder: [],\n groupLabels: {},\n itemLabels: {},\n hiddenItems: [],\n itemOrder: {},\n }\n}\n\nasync function loadRolesPayload(\n em: EntityManager,\n options: { tenantId: string | null; locale: string },\n): Promise<Array<{ id: string; name: string; hasPreference: boolean }>> {\n const roleScope: FilterQuery<Role> = options.tenantId\n ? { $or: [{ tenantId: options.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const roles = await findWithDecryption(\n em,\n Role,\n roleScope,\n { orderBy: { name: 'asc' } },\n { tenantId: options.tenantId, organizationId: null },\n )\n if (roles.length === 0) return []\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: roles.map((r: Role) => r.id),\n tenantId: options.tenantId,\n locale: options.locale,\n })\n return roles.map((role: Role) => ({\n id: role.id,\n name: role.name,\n hasPreference: rolePrefs.has(role.id),\n }))\n}\n\nasync function findRoleInScope(\n em: EntityManager,\n options: { roleId: string; tenantId: string | null },\n): Promise<Role | null> {\n // Scope the DB query so MikroORM only fetches in-scope rows: a role belongs to either\n // the auth tenant or the global (null tenant) pool. Mirrors loadRolesPayload()'s scoping\n // so cross-tenant role rows are never loaded or decrypted into memory in the first place.\n const roleScope: FilterQuery<Role> = options.tenantId\n ? { id: options.roleId, $or: [{ tenantId: options.tenantId }, { tenantId: null }] }\n : { id: options.roleId, tenantId: null }\n const role = await findOneWithDecryption(\n em,\n Role,\n roleScope,\n undefined,\n { tenantId: options.tenantId, organizationId: null },\n )\n if (!role) return null\n // Cross-tenant guard (defense-in-depth): a role belongs to either the auth tenant or the\n // global (null tenant) pool. Reject the lookup otherwise so a multi-tenant deployment can't\n // leak across tenants even if the query above is later changed.\n if (role.tenantId && options.tenantId && role.tenantId !== options.tenantId) return null\n if (role.tenantId && !options.tenantId) return null\n return role\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleIdParam = url.searchParams.get('roleId')\n\n const { locale } = await resolveTranslations()\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const rbac = resolve('rbacService') as any\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n // Role-scoped read: requires `auth.sidebar.manage`.\n if (roleIdParam) {\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n const role = await findRoleInScope(em, { roleId: roleIdParam, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: [role.id],\n tenantId: auth.tenantId ?? null,\n locale,\n })\n const pref = rolePrefs.get(role.id) ?? null\n const rolesPayload = await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n const roleVersion = await loadRoleSidebarPreferenceUpdatedAt(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n return NextResponse.json({\n locale,\n settings: pref\n ? {\n version: pref.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder: pref.groupOrder ?? [],\n groupLabels: pref.groupLabels ?? {},\n itemLabels: pref.itemLabels ?? {},\n hiddenItems: pref.hiddenItems ?? [],\n itemOrder: pref.itemOrder ?? {},\n }\n : emptySettings(),\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'role', roleId: role.id },\n updatedAt: roleVersion?.updatedAt ? roleVersion.updatedAt.toISOString() : null,\n })\n }\n\n // For API key auth, use userId (the actual user) if available\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n const settings = effectiveUserId\n ? await loadSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n : null\n\n const rolesPayload = canApplyToRoles\n ? await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n : []\n\n const userVersion = effectiveUserId\n ? await loadSidebarPreferenceUpdatedAt(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n : null\n\n return NextResponse.json({\n locale,\n settings: {\n version: settings?.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder: settings?.groupOrder ?? [],\n groupLabels: settings?.groupLabels ?? {},\n itemLabels: settings?.itemLabels ?? {},\n hiddenItems: settings?.hiddenItems ?? [],\n itemOrder: settings?.itemOrder ?? {},\n },\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'user' },\n updatedAt: userVersion?.updatedAt ? userVersion.updatedAt.toISOString() : null,\n })\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n // For API key auth, use userId (the actual user) if available\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n if (!effectiveUserId) {\n return NextResponse.json({ error: 'Cannot save preferences: no user associated with this API key' }, { status: 403 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const parsed = sidebarPreferencesInputSchema.safeParse(parsedBody)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const sanitizeRecord = (record?: Record<string, string>) => {\n if (!record) return {}\n const result: Record<string, string> = {}\n for (const [key, value] of Object.entries(record)) {\n const trimmedKey = key.trim()\n const trimmedValue = value.trim()\n if (!trimmedKey || !trimmedValue) continue\n result[trimmedKey] = trimmedValue\n }\n return result\n }\n\n const groupOrderSource = parsed.data.groupOrder ?? []\n const seen = new Set<string>()\n const groupOrder: string[] = []\n for (const id of groupOrderSource) {\n const trimmed = id.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n groupOrder.push(trimmed)\n }\n\n const payload = {\n version: parsed.data.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder,\n groupLabels: sanitizeRecord(parsed.data.groupLabels),\n itemLabels: sanitizeRecord(parsed.data.itemLabels),\n hiddenItems: (() => {\n const source = parsed.data.hiddenItems ?? []\n const seenHidden = new Set<string>()\n const values: string[] = []\n for (const href of source) {\n const trimmed = href.trim()\n if (!trimmed || seenHidden.has(trimmed)) continue\n seenHidden.add(trimmed)\n values.push(trimmed)\n }\n return values\n })(),\n itemOrder: (() => {\n const source = parsed.data.itemOrder ?? {}\n const out: Record<string, string[]> = {}\n for (const [groupKey, list] of Object.entries(source)) {\n const trimmedGroup = groupKey.trim()\n if (!trimmedGroup) continue\n const seenItem = new Set<string>()\n const values: string[] = []\n for (const itemKey of list) {\n const trimmedItem = itemKey.trim()\n if (!trimmedItem || seenItem.has(trimmedItem)) continue\n seenItem.add(trimmedItem)\n values.push(trimmedItem)\n }\n if (values.length > 0) out[trimmedGroup] = values\n }\n return out\n })(),\n }\n\n const { locale } = await resolveTranslations()\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as any\n const cache = container.resolve('cache') as { deleteByTags?: (tags: string[]) => Promise<unknown> } | undefined\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n const scope = parsed.data.scope ?? { type: 'user' as const }\n\n // Role-scoped write: requires `auth.sidebar.manage` and a role visible to this tenant.\n // applyToRoles/clearRoleIds are forbidden in role scope (validator already rejects them).\n if (scope.type === 'role') {\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n const role = await findRoleInScope(em, { roleId: scope.roleId, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n const existingRolePref = await loadRoleSidebarPreferenceUpdatedAt(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n if (existingRolePref) {\n try {\n enforceCommandOptimisticLock({\n resourceKind: 'auth.role_sidebar_preference',\n resourceId: existingRolePref.id,\n current: existingRolePref.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const saved = await saveRoleSidebarPreference(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n }, payload)\n const savedRoleVersion = await loadRoleSidebarPreferenceUpdatedAt(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n if (cache?.deleteByTags) {\n try {\n await cache.deleteByTags([`nav:sidebar:role:${role.id}`])\n } catch {}\n }\n const rolesPayload = await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n return NextResponse.json({\n locale,\n settings: {\n version: saved?.version ?? payload.version,\n groupOrder: saved?.groupOrder ?? payload.groupOrder,\n groupLabels: saved?.groupLabels ?? payload.groupLabels,\n itemLabels: saved?.itemLabels ?? payload.itemLabels,\n hiddenItems: saved?.hiddenItems ?? payload.hiddenItems,\n itemOrder: saved?.itemOrder ?? payload.itemOrder,\n },\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'role', roleId: role.id },\n updatedAt: savedRoleVersion?.updatedAt ? savedRoleVersion.updatedAt.toISOString() : null,\n appliedRoles: [],\n clearedRoles: [],\n })\n }\n\n const applyToRolesSource = parsed.data.applyToRoles ?? []\n const applyToRoles = Array.from(new Set(applyToRolesSource.map((id) => id.trim()).filter((id) => id.length > 0)))\n const clearRoleIdsSource = parsed.data.clearRoleIds ?? []\n const clearRoleIds = Array.from(new Set(clearRoleIdsSource.map((id) => id.trim()).filter((id) => id.length > 0)))\n\n if ((applyToRoles.length > 0 || clearRoleIds.length > 0) && !canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n\n const existingUserPref = await loadSidebarPreferenceUpdatedAt(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n if (existingUserPref) {\n try {\n enforceCommandOptimisticLock({\n resourceKind: 'auth.sidebar_preference',\n resourceId: existingUserPref.id,\n current: existingUserPref.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const settings = await saveSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n }, payload)\n\n const roleScope: FilterQuery<Role> = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const availableRoles = canApplyToRoles\n ? await findWithDecryption(\n em,\n Role,\n roleScope,\n { orderBy: { name: 'asc' } },\n { tenantId: auth.tenantId ?? null, organizationId: null },\n )\n : []\n const roleMap = new Map<string, Role>(availableRoles.map((role: Role) => [String(role.id), role]))\n\n if (applyToRoles.length > 0) {\n const missing = applyToRoles.filter((id) => !roleMap.has(id))\n if (missing.length) {\n return NextResponse.json({ error: 'Invalid roles', missing }, { status: 400 })\n }\n }\n\n const updatedRoleIds: string[] = []\n const filteredClearRoleIds: string[] = []\n await withAtomicFlush(em, [\n async () => {\n for (const roleId of applyToRoles) {\n const role = roleMap.get(roleId)!\n await saveRoleSidebarPreference(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n }, payload)\n updatedRoleIds.push(role.id)\n }\n\n const clearTargets = clearRoleIds.filter((id) => !updatedRoleIds.includes(id) && !applyToRoles.includes(id))\n filteredClearRoleIds.push(...clearTargets)\n\n if (filteredClearRoleIds.length > 0) {\n // Cross-locale: role preferences are unique per (role, tenantId); keep the delete\n // filter aligned with save/load helpers so a clear from one locale does not leave\n // a row created under another locale orphaned.\n await em.nativeDelete(RoleSidebarPreference, {\n role: { $in: filteredClearRoleIds },\n tenantId: auth.tenantId ?? null,\n })\n }\n },\n ], { transaction: true })\n\n if (filteredClearRoleIds.length > 0 && cache?.deleteByTags) {\n try {\n await cache.deleteByTags(filteredClearRoleIds.map((roleId) => `nav:sidebar:role:${roleId}`))\n } catch {}\n }\n\n if (cache?.deleteByTags) {\n const tags = [\n `nav:sidebar:user:${auth.sub}`,\n `nav:sidebar:scope:${auth.sub}:${auth.tenantId ?? 'null'}:${auth.orgId ?? 'null'}:${locale}`,\n ...updatedRoleIds.map((roleId) => `nav:sidebar:role:${roleId}`),\n ]\n try {\n await cache.deleteByTags(tags)\n } catch {}\n }\n\n let rolesPayload: Array<{ id: string; name: string; hasPreference: boolean }> = []\n if (canApplyToRoles) {\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: availableRoles.map((role: Role) => role.id),\n tenantId: auth.tenantId ?? null,\n locale,\n })\n rolesPayload = availableRoles.map((role: Role) => ({\n id: role.id,\n name: role.name,\n hasPreference: rolePrefs.has(role.id),\n }))\n }\n\n const savedUserVersion = await loadSidebarPreferenceUpdatedAt(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n\n return NextResponse.json({\n locale,\n settings,\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'user' },\n updatedAt: savedUserVersion?.updatedAt ? savedUserVersion.updatedAt.toISOString() : null,\n appliedRoles: updatedRoleIds,\n clearedRoles: filteredClearRoleIds,\n })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleIdParam = url.searchParams.get('roleId')\n if (!roleIdParam) {\n return NextResponse.json({ error: 'roleId query parameter is required' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as any\n const cache = container.resolve('cache') as { deleteByTags?: (tags: string[]) => Promise<unknown> } | undefined\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n\n const role = await findRoleInScope(em, { roleId: roleIdParam, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n // Cross-locale: keep the delete filter aligned with save/load helpers (no locale).\n await em.nativeDelete(RoleSidebarPreference, {\n role: role.id,\n tenantId: auth.tenantId ?? null,\n })\n\n if (cache?.deleteByTags) {\n try {\n await cache.deleteByTags([`nav:sidebar:role:${role.id}`])\n } catch {}\n }\n\n return NextResponse.json({ ok: true, scope: { type: 'role', roleId: role.id } })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Sidebar preferences',\n methods: {\n GET: {\n summary: 'Get sidebar preferences',\n description: 'Returns sidebar customization for the current user (default) or the specified role (`?roleId=\u2026`, requires `auth.sidebar.manage`).',\n responses: [\n { status: 200, description: 'Current sidebar configuration', schema: sidebarPreferencesResponseSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features for role-scope read', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update sidebar preferences',\n description: 'Updates sidebar configuration. With `scope.type === \"user\"` (default) writes the calling user\\'s personal preferences and may optionally apply the same settings to selected roles via `applyToRoles[]`. With `scope.type === \"role\"` writes the named role variant directly (requires `auth.sidebar.manage`); `applyToRoles[]` and `clearRoleIds[]` are rejected in this mode.',\n requestBody: {\n contentType: 'application/json',\n schema: sidebarPreferencesInputSchema,\n },\n responses: [\n { status: 200, description: 'Preferences saved', schema: sidebarPreferencesUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: sidebarErrorSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features for role-wide updates', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete a role sidebar variant',\n description: 'Removes the role variant for the current tenant + locale. Idempotent. Requires `auth.sidebar.manage`.',\n responses: [\n { status: 200, description: 'Variant deleted (or never existed)', schema: sidebarPreferencesDeleteResponseSchema },\n { status: 400, description: 'Missing roleId query parameter', schema: sidebarErrorSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n },\n}\n"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n sidebarPreferencesInputSchema,\n sidebarPreferencesScopeSchema,\n} from '../../../data/validators'\nimport {\n findSidebarPreference,\n loadRoleSidebarPreferenceUpdatedAt,\n loadRoleSidebarPreferences,\n loadSidebarPreferenceUpdatedAt,\n saveRoleSidebarPreference,\n saveSidebarPreference,\n} from '../../../services/sidebarPreferencesService'\nimport { SIDEBAR_PREFERENCES_VERSION } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { Role, RoleSidebarPreference } from '../../../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { z } from 'zod'\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true, requireFeatures: ['auth.sidebar.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['auth.sidebar.manage'] },\n}\n\nconst sidebarSettingsSchema = z.object({\n version: z.number().int().positive(),\n groupOrder: z.array(z.string()),\n groupLabels: z.record(z.string(), z.string()),\n itemLabels: z.record(z.string(), z.string()),\n hiddenItems: z.array(z.string()),\n itemOrder: z.record(z.string(), z.array(z.string())),\n})\n\nconst sidebarRoleEntrySchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n hasPreference: z.boolean(),\n})\n\nconst sidebarPreferencesResponseSchema = z.object({\n locale: z.string(),\n settings: sidebarSettingsSchema,\n canApplyToRoles: z.boolean(),\n roles: z.array(sidebarRoleEntrySchema),\n scope: sidebarPreferencesScopeSchema,\n updatedAt: z.string().datetime().nullable(),\n})\n\nconst sidebarPreferencesUpdateResponseSchema = sidebarPreferencesResponseSchema.extend({\n appliedRoles: z.array(z.string().uuid()),\n clearedRoles: z.array(z.string().uuid()),\n})\n\nconst sidebarPreferencesDeleteResponseSchema = z.object({\n ok: z.literal(true),\n scope: sidebarPreferencesScopeSchema,\n})\n\nconst sidebarErrorSchema = z.object({\n error: z.string(),\n})\n\nconst FEATURE_MANAGE = 'auth.sidebar.manage'\n\ntype EmptySettings = {\n version: number\n groupOrder: string[]\n groupLabels: Record<string, string>\n itemLabels: Record<string, string>\n hiddenItems: string[]\n itemOrder: Record<string, string[]>\n}\n\nfunction emptySettings(): EmptySettings {\n return {\n version: SIDEBAR_PREFERENCES_VERSION,\n groupOrder: [],\n groupLabels: {},\n itemLabels: {},\n hiddenItems: [],\n itemOrder: {},\n }\n}\n\nasync function loadRolesPayload(\n em: EntityManager,\n options: { tenantId: string | null; locale: string },\n): Promise<Array<{ id: string; name: string; hasPreference: boolean }>> {\n const roleScope: FilterQuery<Role> = options.tenantId\n ? { $or: [{ tenantId: options.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const roles = await findWithDecryption(\n em,\n Role,\n roleScope,\n { orderBy: { name: 'asc' } },\n { tenantId: options.tenantId, organizationId: null },\n )\n if (roles.length === 0) return []\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: roles.map((r: Role) => r.id),\n tenantId: options.tenantId,\n locale: options.locale,\n })\n return roles.map((role: Role) => ({\n id: role.id,\n name: role.name,\n hasPreference: rolePrefs.has(role.id),\n }))\n}\n\nasync function findRoleInScope(\n em: EntityManager,\n options: { roleId: string; tenantId: string | null },\n): Promise<Role | null> {\n // Scope the DB query so MikroORM only fetches in-scope rows: a role belongs to either\n // the auth tenant or the global (null tenant) pool. Mirrors loadRolesPayload()'s scoping\n // so cross-tenant role rows are never loaded or decrypted into memory in the first place.\n const roleScope: FilterQuery<Role> = options.tenantId\n ? { id: options.roleId, $or: [{ tenantId: options.tenantId }, { tenantId: null }] }\n : { id: options.roleId, tenantId: null }\n const role = await findOneWithDecryption(\n em,\n Role,\n roleScope,\n undefined,\n { tenantId: options.tenantId, organizationId: null },\n )\n if (!role) return null\n // Cross-tenant guard (defense-in-depth): a role belongs to either the auth tenant or the\n // global (null tenant) pool. Reject the lookup otherwise so a multi-tenant deployment can't\n // leak across tenants even if the query above is later changed.\n if (role.tenantId && options.tenantId && role.tenantId !== options.tenantId) return null\n if (role.tenantId && !options.tenantId) return null\n return role\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleIdParam = url.searchParams.get('roleId')\n\n const { locale } = await resolveTranslations()\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const rbac = resolve('rbacService') as any\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n // Role-scoped read: requires `auth.sidebar.manage`.\n if (roleIdParam) {\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n const role = await findRoleInScope(em, { roleId: roleIdParam, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: [role.id],\n tenantId: auth.tenantId ?? null,\n locale,\n })\n const pref = rolePrefs.get(role.id) ?? null\n const rolesPayload = await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n const roleVersion = await loadRoleSidebarPreferenceUpdatedAt(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n return NextResponse.json({\n locale,\n settings: pref\n ? {\n version: pref.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder: pref.groupOrder ?? [],\n groupLabels: pref.groupLabels ?? {},\n itemLabels: pref.itemLabels ?? {},\n hiddenItems: pref.hiddenItems ?? [],\n itemOrder: pref.itemOrder ?? {},\n }\n : emptySettings(),\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'role', roleId: role.id },\n updatedAt: roleVersion?.updatedAt ? roleVersion.updatedAt.toISOString() : null,\n })\n }\n\n // For API key auth, use userId (the actual user) if available\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n const settings = effectiveUserId\n ? await findSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n : null\n\n const rolesPayload = canApplyToRoles\n ? await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n : []\n\n const userVersion = effectiveUserId\n ? await loadSidebarPreferenceUpdatedAt(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n : null\n\n return NextResponse.json({\n locale,\n settings: {\n version: settings?.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder: settings?.groupOrder ?? [],\n groupLabels: settings?.groupLabels ?? {},\n itemLabels: settings?.itemLabels ?? {},\n hiddenItems: settings?.hiddenItems ?? [],\n itemOrder: settings?.itemOrder ?? {},\n },\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'user' },\n updatedAt: userVersion?.updatedAt ? userVersion.updatedAt.toISOString() : null,\n })\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n // For API key auth, use userId (the actual user) if available\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n if (!effectiveUserId) {\n return NextResponse.json({ error: 'Cannot save preferences: no user associated with this API key' }, { status: 403 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n const parsed = sidebarPreferencesInputSchema.safeParse(parsedBody)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const sanitizeRecord = (record?: Record<string, string>) => {\n if (!record) return {}\n const result: Record<string, string> = {}\n for (const [key, value] of Object.entries(record)) {\n const trimmedKey = key.trim()\n const trimmedValue = value.trim()\n if (!trimmedKey || !trimmedValue) continue\n result[trimmedKey] = trimmedValue\n }\n return result\n }\n\n const groupOrderSource = parsed.data.groupOrder ?? []\n const seen = new Set<string>()\n const groupOrder: string[] = []\n for (const id of groupOrderSource) {\n const trimmed = id.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n groupOrder.push(trimmed)\n }\n\n const payload = {\n version: parsed.data.version ?? SIDEBAR_PREFERENCES_VERSION,\n groupOrder,\n groupLabels: sanitizeRecord(parsed.data.groupLabels),\n itemLabels: sanitizeRecord(parsed.data.itemLabels),\n hiddenItems: (() => {\n const source = parsed.data.hiddenItems ?? []\n const seenHidden = new Set<string>()\n const values: string[] = []\n for (const href of source) {\n const trimmed = href.trim()\n if (!trimmed || seenHidden.has(trimmed)) continue\n seenHidden.add(trimmed)\n values.push(trimmed)\n }\n return values\n })(),\n itemOrder: (() => {\n const source = parsed.data.itemOrder ?? {}\n const out: Record<string, string[]> = {}\n for (const [groupKey, list] of Object.entries(source)) {\n const trimmedGroup = groupKey.trim()\n if (!trimmedGroup) continue\n const seenItem = new Set<string>()\n const values: string[] = []\n for (const itemKey of list) {\n const trimmedItem = itemKey.trim()\n if (!trimmedItem || seenItem.has(trimmedItem)) continue\n seenItem.add(trimmedItem)\n values.push(trimmedItem)\n }\n if (values.length > 0) out[trimmedGroup] = values\n }\n return out\n })(),\n }\n\n const { locale } = await resolveTranslations()\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as any\n const cache = container.resolve('cache') as { deleteByTags?: (tags: string[]) => Promise<unknown> } | undefined\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n\n const scope = parsed.data.scope ?? { type: 'user' as const }\n\n // Role-scoped write: requires `auth.sidebar.manage` and a role visible to this tenant.\n // applyToRoles/clearRoleIds are forbidden in role scope (validator already rejects them).\n if (scope.type === 'role') {\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n const role = await findRoleInScope(em, { roleId: scope.roleId, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n const existingRolePref = await loadRoleSidebarPreferenceUpdatedAt(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n if (existingRolePref) {\n try {\n enforceCommandOptimisticLock({\n resourceKind: 'auth.role_sidebar_preference',\n resourceId: existingRolePref.id,\n current: existingRolePref.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const saved = await saveRoleSidebarPreference(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n }, payload)\n const savedRoleVersion = await loadRoleSidebarPreferenceUpdatedAt(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n })\n if (cache?.deleteByTags) {\n try {\n await cache.deleteByTags([`nav:sidebar:role:${role.id}`])\n } catch {}\n }\n const rolesPayload = await loadRolesPayload(em, { tenantId: auth.tenantId ?? null, locale })\n return NextResponse.json({\n locale,\n settings: {\n version: saved?.version ?? payload.version,\n groupOrder: saved?.groupOrder ?? payload.groupOrder,\n groupLabels: saved?.groupLabels ?? payload.groupLabels,\n itemLabels: saved?.itemLabels ?? payload.itemLabels,\n hiddenItems: saved?.hiddenItems ?? payload.hiddenItems,\n itemOrder: saved?.itemOrder ?? payload.itemOrder,\n },\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'role', roleId: role.id },\n updatedAt: savedRoleVersion?.updatedAt ? savedRoleVersion.updatedAt.toISOString() : null,\n appliedRoles: [],\n clearedRoles: [],\n })\n }\n\n const applyToRolesSource = parsed.data.applyToRoles ?? []\n const applyToRoles = Array.from(new Set(applyToRolesSource.map((id) => id.trim()).filter((id) => id.length > 0)))\n const clearRoleIdsSource = parsed.data.clearRoleIds ?? []\n const clearRoleIds = Array.from(new Set(clearRoleIdsSource.map((id) => id.trim()).filter((id) => id.length > 0)))\n\n if ((applyToRoles.length > 0 || clearRoleIds.length > 0) && !canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n\n const existingUserPref = await loadSidebarPreferenceUpdatedAt(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n if (existingUserPref) {\n try {\n enforceCommandOptimisticLock({\n resourceKind: 'auth.sidebar_preference',\n resourceId: existingUserPref.id,\n current: existingUserPref.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const settings = await saveSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n }, payload)\n\n const roleScope: FilterQuery<Role> = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n const availableRoles = canApplyToRoles\n ? await findWithDecryption(\n em,\n Role,\n roleScope,\n { orderBy: { name: 'asc' } },\n { tenantId: auth.tenantId ?? null, organizationId: null },\n )\n : []\n const roleMap = new Map<string, Role>(availableRoles.map((role: Role) => [String(role.id), role]))\n\n if (applyToRoles.length > 0) {\n const missing = applyToRoles.filter((id) => !roleMap.has(id))\n if (missing.length) {\n return NextResponse.json({ error: 'Invalid roles', missing }, { status: 400 })\n }\n }\n\n const updatedRoleIds: string[] = []\n const filteredClearRoleIds: string[] = []\n await withAtomicFlush(em, [\n async () => {\n for (const roleId of applyToRoles) {\n const role = roleMap.get(roleId)!\n await saveRoleSidebarPreference(em, {\n roleId: role.id,\n tenantId: auth.tenantId ?? null,\n locale,\n }, payload)\n updatedRoleIds.push(role.id)\n }\n\n const clearTargets = clearRoleIds.filter((id) => !updatedRoleIds.includes(id) && !applyToRoles.includes(id))\n filteredClearRoleIds.push(...clearTargets)\n\n if (filteredClearRoleIds.length > 0) {\n // Cross-locale: role preferences are unique per (role, tenantId); keep the delete\n // filter aligned with save/load helpers so a clear from one locale does not leave\n // a row created under another locale orphaned.\n await em.nativeDelete(RoleSidebarPreference, {\n role: { $in: filteredClearRoleIds },\n tenantId: auth.tenantId ?? null,\n })\n }\n },\n ], { transaction: true })\n\n if (filteredClearRoleIds.length > 0 && cache?.deleteByTags) {\n try {\n await cache.deleteByTags(filteredClearRoleIds.map((roleId) => `nav:sidebar:role:${roleId}`))\n } catch {}\n }\n\n if (cache?.deleteByTags) {\n const tags = [\n `nav:sidebar:user:${auth.sub}`,\n `nav:sidebar:scope:${auth.sub}:${auth.tenantId ?? 'null'}:${auth.orgId ?? 'null'}:${locale}`,\n ...updatedRoleIds.map((roleId) => `nav:sidebar:role:${roleId}`),\n ]\n try {\n await cache.deleteByTags(tags)\n } catch {}\n }\n\n let rolesPayload: Array<{ id: string; name: string; hasPreference: boolean }> = []\n if (canApplyToRoles) {\n const rolePrefs = await loadRoleSidebarPreferences(em, {\n roleIds: availableRoles.map((role: Role) => role.id),\n tenantId: auth.tenantId ?? null,\n locale,\n })\n rolesPayload = availableRoles.map((role: Role) => ({\n id: role.id,\n name: role.name,\n hasPreference: rolePrefs.has(role.id),\n }))\n }\n\n const savedUserVersion = await loadSidebarPreferenceUpdatedAt(em, {\n userId: effectiveUserId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n locale,\n })\n\n return NextResponse.json({\n locale,\n settings,\n canApplyToRoles,\n roles: rolesPayload,\n scope: { type: 'user' },\n updatedAt: savedUserVersion?.updatedAt ? savedUserVersion.updatedAt.toISOString() : null,\n appliedRoles: updatedRoleIds,\n clearedRoles: filteredClearRoleIds,\n })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const roleIdParam = url.searchParams.get('roleId')\n if (!roleIdParam) {\n return NextResponse.json({ error: 'roleId query parameter is required' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as any\n const cache = container.resolve('cache') as { deleteByTags?: (tags: string[]) => Promise<unknown> } | undefined\n\n const canApplyToRoles = await rbac.userHasAllFeatures?.(\n auth.sub,\n [FEATURE_MANAGE],\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n ) ?? false\n if (!canApplyToRoles) {\n return NextResponse.json({ error: 'Forbidden', requiredFeatures: [FEATURE_MANAGE] }, { status: 403 })\n }\n\n const role = await findRoleInScope(em, { roleId: roleIdParam, tenantId: auth.tenantId ?? null })\n if (!role) {\n return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n }\n\n // Cross-locale: keep the delete filter aligned with save/load helpers (no locale).\n await em.nativeDelete(RoleSidebarPreference, {\n role: role.id,\n tenantId: auth.tenantId ?? null,\n })\n\n if (cache?.deleteByTags) {\n try {\n await cache.deleteByTags([`nav:sidebar:role:${role.id}`])\n } catch {}\n }\n\n return NextResponse.json({ ok: true, scope: { type: 'role', roleId: role.id } })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Sidebar preferences',\n methods: {\n GET: {\n summary: 'Get sidebar preferences',\n description: 'Returns sidebar customization for the current user (default) or the specified role (`?roleId=\u2026`, requires `auth.sidebar.manage`).',\n responses: [\n { status: 200, description: 'Current sidebar configuration', schema: sidebarPreferencesResponseSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features for role-scope read', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update sidebar preferences',\n description: 'Updates sidebar configuration. With `scope.type === \"user\"` (default) writes the calling user\\'s personal preferences and may optionally apply the same settings to selected roles via `applyToRoles[]`. With `scope.type === \"role\"` writes the named role variant directly (requires `auth.sidebar.manage`); `applyToRoles[]` and `clearRoleIds[]` are rejected in this mode.',\n requestBody: {\n contentType: 'application/json',\n schema: sidebarPreferencesInputSchema,\n },\n responses: [\n { status: 200, description: 'Preferences saved', schema: sidebarPreferencesUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: sidebarErrorSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features for role-wide updates', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete a role sidebar variant',\n description: 'Removes the role variant for the current tenant + locale. Idempotent. Requires `auth.sidebar.manage`.',\n responses: [\n { status: 200, description: 'Variant deleted (or never existed)', schema: sidebarPreferencesDeleteResponseSchema },\n { status: 400, description: 'Missing roleId query parameter', schema: sidebarErrorSchema },\n { status: 401, description: 'Unauthorized', schema: sidebarErrorSchema },\n { status: 403, description: 'Missing features', schema: sidebarErrorSchema },\n { status: 404, description: 'Role not found in current tenant scope', schema: sidebarErrorSchema },\n ],\n },\n },\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["rolesPayload"]
7
7
  }
@@ -20,8 +20,8 @@ import { Role } from "@open-mercato/core/modules/auth/data/entities";
20
20
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
21
21
  import {
22
22
  applySidebarPreference,
23
- loadFirstRoleSidebarPreference,
24
- loadSidebarPreference
23
+ findSidebarPreference,
24
+ loadFirstRoleSidebarPreference
25
25
  } from "@open-mercato/core/modules/auth/services/sidebarPreferencesService";
26
26
  function groupBackendRoutesByModule(routes) {
27
27
  return Array.from(
@@ -293,7 +293,7 @@ async function resolveBackendChromePayload({
293
293
  }
294
294
  const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub;
295
295
  if (effectiveUserId) {
296
- userPreference = await loadSidebarPreference(em, {
296
+ userPreference = await findSidebarPreference(em, {
297
297
  userId: effectiveUserId,
298
298
  tenantId: scopedTenantId,
299
299
  organizationId: scopedOrganizationId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/lib/backendChrome.tsx"],
4
- "sourcesContent": ["import * as React from 'react'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { BackendRouteManifestEntry } from '@open-mercato/shared/modules/registry'\nimport type {\n BackendChromePayload,\n BackendChromeNavGroup,\n BackendChromeNavItem,\n BackendChromeSectionGroup,\n BackendChromeSectionItem,\n} from '@open-mercato/shared/modules/navigation/backendChrome'\nimport {\n buildAdminNav,\n buildSettingsSections,\n computeSettingsPathPrefixes,\n convertToSectionNavGroups,\n type AdminNavItem,\n} from '@open-mercato/ui/backend/utils/nav'\nimport { resolveRegisteredLucideIconNode } from '@open-mercato/ui/backend/icons/lucideRegistry'\nimport { profilePathPrefixes, profileSections } from './profile-sections'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getNavGroupOrderOverride } from '@open-mercato/shared/modules/overrides'\nimport {\n getSelectedOrganizationFromRequest,\n resolveFeatureCheckContext,\n} from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { CustomEntity } from '@open-mercato/core/modules/entities/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n applySidebarPreference,\n loadFirstRoleSidebarPreference,\n loadSidebarPreference,\n} from '@open-mercato/core/modules/auth/services/sidebarPreferencesService'\nimport type { SidebarPreferencesSettings } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\n\ntype TranslationFn = (key: string | undefined, fallback: string) => string\n\ntype RouteModule = {\n id: string\n backendRoutes?: BackendRouteManifestEntry[]\n}\n\nexport function groupBackendRoutesByModule(routes: BackendRouteManifestEntry[]): RouteModule[] {\n return Array.from(\n routes.reduce((grouped, route) => {\n const list = grouped.get(route.moduleId) ?? []\n list.push(route)\n grouped.set(route.moduleId, list)\n return grouped\n }, new Map<string, BackendRouteManifestEntry[]>()),\n ).map(([id, backendRoutes]) => ({ id, backendRoutes }))\n}\n\ntype SerializableSectionItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: React.ReactNode\n order?: number\n children?: SerializableSectionItem[]\n}\n\ntype SerializableSectionGroup = {\n id: string\n label: string\n labelKey?: string\n order?: number\n items: SerializableSectionItem[]\n}\n\ntype ResolvedNavItem = Omit<BackendChromeNavItem, 'defaultTitle' | 'children'> & {\n defaultTitle: string\n children?: ResolvedNavItem[]\n}\n\ntype ResolveBackendChromePayloadArgs = {\n auth: Exclude<AuthContext, null>\n locale: string\n modules: RouteModule[]\n translate: TranslationFn\n request?: Request\n selectedOrganizationId?: string | null\n selectedTenantId?: string | null\n}\n\n/**\n * Settings section weights, keyed by the untranslated group id each page declares as `pageGroupKey`.\n *\n * Mirrors `defaultGroupOrder` above: an id, never a rendered label, so the panel keeps its intended\n * order in every locale and an app-side module can place its own section deterministically (#4843).\n */\nexport const settingsSectionOrder: Record<string, number> = {\n 'settings.sections.system': 1,\n 'settings.sections.auth': 2,\n 'customer_accounts.settings.section': 3,\n 'settings.sections.dataDesigner': 4,\n 'settings.sections.moduleConfigs': 5,\n 'currencies.nav.group': 6,\n 'settings.sections.directory': 7,\n 'settings.sections.featureToggles': 8,\n}\n\ntype NavGroupWithWeight = Omit<BackendChromeNavGroup, 'id' | 'defaultName' | 'items'> & {\n id: string\n defaultName: string\n items: ResolvedNavItem[]\n weight: number\n}\n\nlet renderToStaticMarkupPromise: Promise<typeof import('react-dom/server')> | null = null\n\nasync function serializeIconMarkup(icon: React.ReactNode | undefined): Promise<string | undefined> {\n if (!icon) return undefined\n if (!renderToStaticMarkupPromise) {\n renderToStaticMarkupPromise = import('react-dom/server')\n }\n const { renderToStaticMarkup } = await renderToStaticMarkupPromise\n\n const normalizedIcon = typeof icon === 'string'\n ? resolveRegisteredLucideIconNode(icon, 'size-4')\n : icon\n\n if (!normalizedIcon) return undefined\n\n try {\n const markup = renderToStaticMarkup(<>{normalizedIcon}</>)\n return markup.trim().length > 0 ? markup : undefined\n } catch {\n // Some icon values may be client-only component references after dependency upgrades.\n // Avoid taking down the entire nav payload because one icon cannot be rendered server-side.\n return undefined\n }\n}\n\nconst NAV_ITEM_FALLBACK_WEIGHT = 10_000\n\n/**\n * The weight a nav entry sorts by, using the same `priority ?? order` precedence as `buildAdminNav`.\n *\n * `serializeNavItem` emits this resolved number rather than the raw declaration, including the\n * fallback, so a consumer that re-sorts by the field it receives lands on the order it was served in.\n * Emitting the raw `priority ?? order` would leave `order` undefined on any page declaring neither \u2014\n * and the `(a.order ?? 0) - (b.order ?? 0)` idiom this codebase uses elsewhere would then hoist those\n * pages to the top instead of leaving them last (#4845).\n */\nfunction resolveNavItemWeight(item: AdminNavItem): number {\n return item.priority ?? item.order ?? NAV_ITEM_FALLBACK_WEIGHT\n}\n\nfunction sortNavItemsByWeight(items: AdminNavItem[]): AdminNavItem[] {\n return [...items].sort((a, b) => {\n const weightDifference = resolveNavItemWeight(a) - resolveNavItemWeight(b)\n if (weightDifference !== 0) return weightDifference\n return a.title.localeCompare(b.title)\n })\n}\n\nasync function serializeNavItem(item: AdminNavItem): Promise<ResolvedNavItem> {\n return {\n id: item.href,\n href: item.href,\n title: item.title,\n defaultTitle: item.defaultTitle,\n enabled: item.enabled,\n hidden: item.hidden,\n pageContext: item.pageContext,\n iconName: typeof item.icon === 'string' ? item.icon : undefined,\n iconMarkup: await serializeIconMarkup(item.icon),\n order: resolveNavItemWeight(item),\n children: item.children\n ? await Promise.all(sortNavItemsByWeight(item.children).map((child) => serializeNavItem(child)))\n : undefined,\n }\n}\n\nconst defaultGroupOrder = [\n 'customers.nav.group',\n 'catalog.nav.group',\n 'customers~sales.nav.group',\n 'wms.nav.group',\n 'resources.nav.group',\n 'staff.nav.group',\n 'entities.nav.group',\n 'directory.nav.group',\n 'attachments.nav.group',\n]\n\n/**\n * Group ids ranked ahead of everything else, most significant first.\n *\n * An app may prepend its own ids via `overrides.nav.groupOrder` in `modules.ts`; ids it does not name\n * keep the ordering they have today. With no override configured this returns `defaultGroupOrder`\n * itself, so ordering is unchanged for every existing install.\n */\nfunction resolveGroupOrder(): string[] {\n const override = getNavGroupOrderOverride()\n if (!override || override.length === 0) return defaultGroupOrder\n const overridden = new Set(override)\n return [...override, ...defaultGroupOrder.filter((id) => !overridden.has(id))]\n}\n\nfunction normalizeGroupWeights(groups: NavGroupWithWeight[]): NavGroupWithWeight[] {\n const groupOrder = resolveGroupOrder()\n const groupOrderIndex = new Map(groupOrder.map((id, index) => [id, index]))\n groups.sort((a, b) => {\n const aIndex = groupOrderIndex.get(a.id)\n const bIndex = groupOrderIndex.get(b.id)\n if (aIndex !== undefined || bIndex !== undefined) {\n if (aIndex === undefined) return 1\n if (bIndex === undefined) return -1\n if (aIndex !== bIndex) return aIndex - bIndex\n }\n if (a.weight !== b.weight) return a.weight - b.weight\n return a.name.localeCompare(b.name)\n })\n const defaultGroupCount = groupOrder.length\n groups.forEach((group, index) => {\n const rank = groupOrderIndex.get(group.id)\n const fallbackWeight = typeof group.weight === 'number' ? group.weight : NAV_ITEM_FALLBACK_WEIGHT\n group.weight =\n (rank !== undefined ? rank : defaultGroupCount + index) * 1_000_000 +\n Math.min(Math.max(fallbackWeight, 0), 999_999)\n })\n return groups\n}\n\nasync function groupEntries(entries: AdminNavItem[]): Promise<NavGroupWithWeight[]> {\n const groupMap = new Map<string, Omit<NavGroupWithWeight, 'items'> & { entries: AdminNavItem[] }>()\n for (const entry of entries) {\n const weight = resolveNavItemWeight(entry)\n const existing = groupMap.get(entry.groupId)\n if (existing) {\n existing.entries.push(entry)\n if (weight < existing.weight) existing.weight = weight\n continue\n }\n groupMap.set(entry.groupId, {\n id: entry.groupId,\n name: entry.group,\n defaultName: entry.groupDefaultName,\n entries: [entry],\n weight,\n })\n }\n const groups: NavGroupWithWeight[] = []\n for (const { entries: groupItems, ...group } of groupMap.values()) {\n groups.push({\n ...group,\n items: await Promise.all(sortNavItemsByWeight(groupItems).map((entry) => serializeNavItem(entry))),\n })\n }\n return normalizeGroupWeights(groups)\n}\n\nfunction adoptSidebarDefaults(groups: NavGroupWithWeight[]): NavGroupWithWeight[] {\n const adoptItems = (items: ResolvedNavItem[]): ResolvedNavItem[] =>\n items.map((item) => ({\n ...item,\n defaultTitle: item.title,\n children: item.children ? adoptItems(item.children) : undefined,\n }))\n\n return groups.map((group) => ({\n ...group,\n defaultName: group.name,\n items: adoptItems(group.items),\n }))\n}\n\nasync function serializeSectionItem(item: {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: React.ReactNode\n order?: number\n children?: SerializableSectionItem[]\n}): Promise<BackendChromeSectionItem> {\n return {\n id: item.id,\n label: item.label,\n labelKey: item.labelKey,\n href: item.href,\n order: item.order,\n iconName: typeof item.icon === 'string' ? item.icon : undefined,\n iconMarkup: await serializeIconMarkup(item.icon),\n children: item.children ? await Promise.all(item.children.map((child) => serializeSectionItem(child))) : undefined,\n }\n}\n\nasync function serializeSectionGroups(groups: SerializableSectionGroup[]): Promise<BackendChromeSectionGroup[]> {\n return Promise.all(groups.map(async (group) => ({\n id: group.id,\n label: group.label,\n labelKey: group.labelKey,\n order: group.order,\n items: await Promise.all(group.items.map((item) => serializeSectionItem(item))),\n })))\n}\n\nasync function loadScopedContainer(): Promise<AwilixContainer> {\n return createRequestContainer()\n}\n\nexport async function resolveBackendChromePayload({\n auth,\n locale,\n modules,\n translate,\n request,\n selectedOrganizationId,\n selectedTenantId,\n}: ResolveBackendChromePayloadArgs): Promise<BackendChromePayload> {\n const container = await loadScopedContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as {\n getEffectiveFeatures: (userId: string, scope: { tenantId: string | null; organizationId: string | null }) => Promise<string[]>\n userHasAllFeatures: (userId: string, required: string[], scope: { tenantId: string | null; organizationId: string | null }) => Promise<boolean>\n }\n\n let scopedOrganizationId: string | null = auth.orgId ?? null\n let scopedTenantId: string | null = auth.tenantId ?? null\n // The organization the caller actually *selected*, as distinct from the one the scope resolver fell\n // back to. `resolveFeatureCheckContext` resolves `organizationId` to `auth.orgId` when no concrete\n // organization is selected \u2014 which is precisely what an all-organizations view produces \u2014 so the\n // resolved id cannot answer \"which organization am I viewing\".\n let concretelySelectedOrganizationId: string | null = null\n let allowNavigation = true\n\n try {\n const { organizationId, scope, allowedOrganizationIds } = await resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId: selectedOrganizationId,\n tenantId: selectedTenantId,\n })\n scopedOrganizationId = organizationId\n scopedTenantId = scope.tenantId ?? auth.tenantId ?? null\n concretelySelectedOrganizationId = scope.selectedId ?? null\n if (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length === 0) {\n allowNavigation = false\n }\n } catch {\n scopedOrganizationId = auth.orgId ?? null\n scopedTenantId = auth.tenantId ?? null\n concretelySelectedOrganizationId = null\n }\n\n const grantedFeatures = allowNavigation\n ? await rbac.getEffectiveFeatures(auth.sub, {\n tenantId: scopedTenantId,\n organizationId: scopedOrganizationId,\n })\n : []\n const featureChecker = async (features: string[]): Promise<string[]> => {\n if (!allowNavigation || !features.length) return []\n const context = {\n tenantId: scopedTenantId ?? auth.tenantId ?? null,\n organizationId: scopedOrganizationId ?? null,\n }\n const hasAll = await rbac.userHasAllFeatures(auth.sub, features, context)\n if (hasAll) return features\n\n const granted: string[] = []\n for (const feature of features) {\n const hasFeature = await rbac.userHasAllFeatures(auth.sub, [feature], context)\n if (hasFeature) granted.push(feature)\n }\n return granted\n }\n\n let userEntities: Array<{ entityId: string; label: string; href: string }> = []\n if (allowNavigation) {\n try {\n const where: FilterQuery<CustomEntity> = {\n isActive: true,\n showInSidebar: true,\n }\n where.$and = [\n { $or: [{ organizationId: scopedOrganizationId ?? undefined }, { organizationId: null }] },\n { $or: [{ tenantId: scopedTenantId ?? undefined }, { tenantId: null }] },\n ]\n const entities = await em.find(CustomEntity, where, { orderBy: { label: 'asc' } })\n userEntities = entities.map((entity) => ({\n entityId: entity.entityId,\n label: entity.label,\n href: `/backend/entities/user/${encodeURIComponent(entity.entityId)}/records`,\n }))\n } catch {\n userEntities = []\n }\n }\n\n const ctxAuth = {\n roles: auth.roles || [],\n sub: auth.sub,\n tenantId: scopedTenantId,\n orgId: scopedOrganizationId,\n }\n const entries = allowNavigation\n ? await buildAdminNav(\n modules,\n { auth: ctxAuth },\n userEntities,\n translate,\n { checkFeatures: featureChecker },\n )\n : []\n\n let rolePreference: SidebarPreferencesSettings | null = null\n let userPreference: SidebarPreferencesSettings | null = null\n\n if (Array.isArray(auth.roles) && auth.roles.length > 0) {\n const roleRecords = scopedTenantId\n ? await em.find(Role, {\n name: { $in: auth.roles },\n tenantId: scopedTenantId,\n })\n : []\n const roleIds = Array.isArray(roleRecords) ? roleRecords.map((role) => role.id) : []\n if (roleIds.length > 0) {\n rolePreference = await loadFirstRoleSidebarPreference(em, {\n roleIds,\n tenantId: scopedTenantId,\n locale,\n })\n }\n }\n\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n if (effectiveUserId) {\n userPreference = await loadSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: scopedTenantId,\n organizationId: scopedOrganizationId,\n locale,\n })\n }\n\n const baseGroups = await groupEntries(entries)\n const groupsWithRole = rolePreference\n ? applySidebarPreference<NavGroupWithWeight>(baseGroups, rolePreference)\n : baseGroups\n const baseForUser = adoptSidebarDefaults(groupsWithRole)\n const appliedGroups = userPreference\n ? applySidebarPreference<NavGroupWithWeight>(baseForUser, userPreference)\n : baseForUser\n\n const settingsSections = await serializeSectionGroups(\n convertToSectionNavGroups(\n buildSettingsSections(entries, settingsSectionOrder),\n translate,\n ),\n )\n\n const requestOrganizationId = request ? getSelectedOrganizationFromRequest(request) : null\n const fallbackOrganizationId = selectedOrganizationId ?? requestOrganizationId ?? auth.orgId ?? null\n const brandOrganizationId = scopedOrganizationId\n ?? (fallbackOrganizationId && !isAllOrganizationsSelection(fallbackOrganizationId) ? fallbackOrganizationId : null)\n\n let brand: BackendChromePayload['brand'] = null\n // Resolved here rather than left to callers. `brand` only populates when the organization has a\n // logo, so it is a branding channel, not a dependable \"which organization am I viewing\" source.\n // Without this field every downstream app has to fetch `/api/directory/organization-switcher` and\n // walk its tree for the selected id. The row is already loaded below, so the name costs nothing.\n let currentOrganization: BackendChromePayload['currentOrganization'] = null\n if (brandOrganizationId && scopedTenantId) {\n try {\n const organization = await findOneWithDecryption(\n em,\n Organization,\n { id: brandOrganizationId, tenant: scopedTenantId, deletedAt: null },\n undefined,\n { tenantId: scopedTenantId, organizationId: brandOrganizationId },\n )\n // Only when a concrete organization was selected. Under an all-organizations view\n // `brandOrganizationId` still resolves (to the caller's own organization, which is what keeps\n // branding working), so gating on the loaded row alone would misreport the scope.\n if (organization && concretelySelectedOrganizationId === brandOrganizationId) {\n currentOrganization = { id: String(organization.id), name: organization.name }\n }\n if (organization?.logoUrl) {\n brand = {\n name: organization.name,\n logo: {\n src: organization.logoUrl,\n alt: `${organization.name} logo`,\n preserveAspectRatio: !!organization.logoPreserveAspectRatio,\n },\n }\n }\n } catch {\n // Fail soft, as before: a failed organization lookup must not take down the nav payload.\n brand = null\n currentOrganization = null\n }\n }\n\n return {\n groups: appliedGroups.map(({ weight: _weight, ...group }) => group),\n settingsSections,\n settingsPathPrefixes: computeSettingsPathPrefixes(buildSettingsSections(entries, settingsSectionOrder)),\n profileSections: await serializeSectionGroups(profileSections),\n profilePathPrefixes,\n grantedFeatures,\n roles: Array.isArray(auth.roles) ? auth.roles : [],\n brand,\n currentOrganization,\n }\n}\n"],
4
+ "sourcesContent": ["import * as React from 'react'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { BackendRouteManifestEntry } from '@open-mercato/shared/modules/registry'\nimport type {\n BackendChromePayload,\n BackendChromeNavGroup,\n BackendChromeNavItem,\n BackendChromeSectionGroup,\n BackendChromeSectionItem,\n} from '@open-mercato/shared/modules/navigation/backendChrome'\nimport {\n buildAdminNav,\n buildSettingsSections,\n computeSettingsPathPrefixes,\n convertToSectionNavGroups,\n type AdminNavItem,\n} from '@open-mercato/ui/backend/utils/nav'\nimport { resolveRegisteredLucideIconNode } from '@open-mercato/ui/backend/icons/lucideRegistry'\nimport { profilePathPrefixes, profileSections } from './profile-sections'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getNavGroupOrderOverride } from '@open-mercato/shared/modules/overrides'\nimport {\n getSelectedOrganizationFromRequest,\n resolveFeatureCheckContext,\n} from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { CustomEntity } from '@open-mercato/core/modules/entities/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n applySidebarPreference,\n findSidebarPreference,\n loadFirstRoleSidebarPreference,\n} from '@open-mercato/core/modules/auth/services/sidebarPreferencesService'\nimport type { SidebarPreferencesSettings } from '@open-mercato/shared/modules/navigation/sidebarPreferences'\n\ntype TranslationFn = (key: string | undefined, fallback: string) => string\n\ntype RouteModule = {\n id: string\n backendRoutes?: BackendRouteManifestEntry[]\n}\n\nexport function groupBackendRoutesByModule(routes: BackendRouteManifestEntry[]): RouteModule[] {\n return Array.from(\n routes.reduce((grouped, route) => {\n const list = grouped.get(route.moduleId) ?? []\n list.push(route)\n grouped.set(route.moduleId, list)\n return grouped\n }, new Map<string, BackendRouteManifestEntry[]>()),\n ).map(([id, backendRoutes]) => ({ id, backendRoutes }))\n}\n\ntype SerializableSectionItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: React.ReactNode\n order?: number\n children?: SerializableSectionItem[]\n}\n\ntype SerializableSectionGroup = {\n id: string\n label: string\n labelKey?: string\n order?: number\n items: SerializableSectionItem[]\n}\n\ntype ResolvedNavItem = Omit<BackendChromeNavItem, 'defaultTitle' | 'children'> & {\n defaultTitle: string\n children?: ResolvedNavItem[]\n}\n\ntype ResolveBackendChromePayloadArgs = {\n auth: Exclude<AuthContext, null>\n locale: string\n modules: RouteModule[]\n translate: TranslationFn\n request?: Request\n selectedOrganizationId?: string | null\n selectedTenantId?: string | null\n}\n\n/**\n * Settings section weights, keyed by the untranslated group id each page declares as `pageGroupKey`.\n *\n * Mirrors `defaultGroupOrder` above: an id, never a rendered label, so the panel keeps its intended\n * order in every locale and an app-side module can place its own section deterministically (#4843).\n */\nexport const settingsSectionOrder: Record<string, number> = {\n 'settings.sections.system': 1,\n 'settings.sections.auth': 2,\n 'customer_accounts.settings.section': 3,\n 'settings.sections.dataDesigner': 4,\n 'settings.sections.moduleConfigs': 5,\n 'currencies.nav.group': 6,\n 'settings.sections.directory': 7,\n 'settings.sections.featureToggles': 8,\n}\n\ntype NavGroupWithWeight = Omit<BackendChromeNavGroup, 'id' | 'defaultName' | 'items'> & {\n id: string\n defaultName: string\n items: ResolvedNavItem[]\n weight: number\n}\n\nlet renderToStaticMarkupPromise: Promise<typeof import('react-dom/server')> | null = null\n\nasync function serializeIconMarkup(icon: React.ReactNode | undefined): Promise<string | undefined> {\n if (!icon) return undefined\n if (!renderToStaticMarkupPromise) {\n renderToStaticMarkupPromise = import('react-dom/server')\n }\n const { renderToStaticMarkup } = await renderToStaticMarkupPromise\n\n const normalizedIcon = typeof icon === 'string'\n ? resolveRegisteredLucideIconNode(icon, 'size-4')\n : icon\n\n if (!normalizedIcon) return undefined\n\n try {\n const markup = renderToStaticMarkup(<>{normalizedIcon}</>)\n return markup.trim().length > 0 ? markup : undefined\n } catch {\n // Some icon values may be client-only component references after dependency upgrades.\n // Avoid taking down the entire nav payload because one icon cannot be rendered server-side.\n return undefined\n }\n}\n\nconst NAV_ITEM_FALLBACK_WEIGHT = 10_000\n\n/**\n * The weight a nav entry sorts by, using the same `priority ?? order` precedence as `buildAdminNav`.\n *\n * `serializeNavItem` emits this resolved number rather than the raw declaration, including the\n * fallback, so a consumer that re-sorts by the field it receives lands on the order it was served in.\n * Emitting the raw `priority ?? order` would leave `order` undefined on any page declaring neither \u2014\n * and the `(a.order ?? 0) - (b.order ?? 0)` idiom this codebase uses elsewhere would then hoist those\n * pages to the top instead of leaving them last (#4845).\n */\nfunction resolveNavItemWeight(item: AdminNavItem): number {\n return item.priority ?? item.order ?? NAV_ITEM_FALLBACK_WEIGHT\n}\n\nfunction sortNavItemsByWeight(items: AdminNavItem[]): AdminNavItem[] {\n return [...items].sort((a, b) => {\n const weightDifference = resolveNavItemWeight(a) - resolveNavItemWeight(b)\n if (weightDifference !== 0) return weightDifference\n return a.title.localeCompare(b.title)\n })\n}\n\nasync function serializeNavItem(item: AdminNavItem): Promise<ResolvedNavItem> {\n return {\n id: item.href,\n href: item.href,\n title: item.title,\n defaultTitle: item.defaultTitle,\n enabled: item.enabled,\n hidden: item.hidden,\n pageContext: item.pageContext,\n iconName: typeof item.icon === 'string' ? item.icon : undefined,\n iconMarkup: await serializeIconMarkup(item.icon),\n order: resolveNavItemWeight(item),\n children: item.children\n ? await Promise.all(sortNavItemsByWeight(item.children).map((child) => serializeNavItem(child)))\n : undefined,\n }\n}\n\nconst defaultGroupOrder = [\n 'customers.nav.group',\n 'catalog.nav.group',\n 'customers~sales.nav.group',\n 'wms.nav.group',\n 'resources.nav.group',\n 'staff.nav.group',\n 'entities.nav.group',\n 'directory.nav.group',\n 'attachments.nav.group',\n]\n\n/**\n * Group ids ranked ahead of everything else, most significant first.\n *\n * An app may prepend its own ids via `overrides.nav.groupOrder` in `modules.ts`; ids it does not name\n * keep the ordering they have today. With no override configured this returns `defaultGroupOrder`\n * itself, so ordering is unchanged for every existing install.\n */\nfunction resolveGroupOrder(): string[] {\n const override = getNavGroupOrderOverride()\n if (!override || override.length === 0) return defaultGroupOrder\n const overridden = new Set(override)\n return [...override, ...defaultGroupOrder.filter((id) => !overridden.has(id))]\n}\n\nfunction normalizeGroupWeights(groups: NavGroupWithWeight[]): NavGroupWithWeight[] {\n const groupOrder = resolveGroupOrder()\n const groupOrderIndex = new Map(groupOrder.map((id, index) => [id, index]))\n groups.sort((a, b) => {\n const aIndex = groupOrderIndex.get(a.id)\n const bIndex = groupOrderIndex.get(b.id)\n if (aIndex !== undefined || bIndex !== undefined) {\n if (aIndex === undefined) return 1\n if (bIndex === undefined) return -1\n if (aIndex !== bIndex) return aIndex - bIndex\n }\n if (a.weight !== b.weight) return a.weight - b.weight\n return a.name.localeCompare(b.name)\n })\n const defaultGroupCount = groupOrder.length\n groups.forEach((group, index) => {\n const rank = groupOrderIndex.get(group.id)\n const fallbackWeight = typeof group.weight === 'number' ? group.weight : NAV_ITEM_FALLBACK_WEIGHT\n group.weight =\n (rank !== undefined ? rank : defaultGroupCount + index) * 1_000_000 +\n Math.min(Math.max(fallbackWeight, 0), 999_999)\n })\n return groups\n}\n\nasync function groupEntries(entries: AdminNavItem[]): Promise<NavGroupWithWeight[]> {\n const groupMap = new Map<string, Omit<NavGroupWithWeight, 'items'> & { entries: AdminNavItem[] }>()\n for (const entry of entries) {\n const weight = resolveNavItemWeight(entry)\n const existing = groupMap.get(entry.groupId)\n if (existing) {\n existing.entries.push(entry)\n if (weight < existing.weight) existing.weight = weight\n continue\n }\n groupMap.set(entry.groupId, {\n id: entry.groupId,\n name: entry.group,\n defaultName: entry.groupDefaultName,\n entries: [entry],\n weight,\n })\n }\n const groups: NavGroupWithWeight[] = []\n for (const { entries: groupItems, ...group } of groupMap.values()) {\n groups.push({\n ...group,\n items: await Promise.all(sortNavItemsByWeight(groupItems).map((entry) => serializeNavItem(entry))),\n })\n }\n return normalizeGroupWeights(groups)\n}\n\nfunction adoptSidebarDefaults(groups: NavGroupWithWeight[]): NavGroupWithWeight[] {\n const adoptItems = (items: ResolvedNavItem[]): ResolvedNavItem[] =>\n items.map((item) => ({\n ...item,\n defaultTitle: item.title,\n children: item.children ? adoptItems(item.children) : undefined,\n }))\n\n return groups.map((group) => ({\n ...group,\n defaultName: group.name,\n items: adoptItems(group.items),\n }))\n}\n\nasync function serializeSectionItem(item: {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: React.ReactNode\n order?: number\n children?: SerializableSectionItem[]\n}): Promise<BackendChromeSectionItem> {\n return {\n id: item.id,\n label: item.label,\n labelKey: item.labelKey,\n href: item.href,\n order: item.order,\n iconName: typeof item.icon === 'string' ? item.icon : undefined,\n iconMarkup: await serializeIconMarkup(item.icon),\n children: item.children ? await Promise.all(item.children.map((child) => serializeSectionItem(child))) : undefined,\n }\n}\n\nasync function serializeSectionGroups(groups: SerializableSectionGroup[]): Promise<BackendChromeSectionGroup[]> {\n return Promise.all(groups.map(async (group) => ({\n id: group.id,\n label: group.label,\n labelKey: group.labelKey,\n order: group.order,\n items: await Promise.all(group.items.map((item) => serializeSectionItem(item))),\n })))\n}\n\nasync function loadScopedContainer(): Promise<AwilixContainer> {\n return createRequestContainer()\n}\n\nexport async function resolveBackendChromePayload({\n auth,\n locale,\n modules,\n translate,\n request,\n selectedOrganizationId,\n selectedTenantId,\n}: ResolveBackendChromePayloadArgs): Promise<BackendChromePayload> {\n const container = await loadScopedContainer()\n const em = container.resolve('em') as EntityManager\n const rbac = container.resolve('rbacService') as {\n getEffectiveFeatures: (userId: string, scope: { tenantId: string | null; organizationId: string | null }) => Promise<string[]>\n userHasAllFeatures: (userId: string, required: string[], scope: { tenantId: string | null; organizationId: string | null }) => Promise<boolean>\n }\n\n let scopedOrganizationId: string | null = auth.orgId ?? null\n let scopedTenantId: string | null = auth.tenantId ?? null\n // The organization the caller actually *selected*, as distinct from the one the scope resolver fell\n // back to. `resolveFeatureCheckContext` resolves `organizationId` to `auth.orgId` when no concrete\n // organization is selected \u2014 which is precisely what an all-organizations view produces \u2014 so the\n // resolved id cannot answer \"which organization am I viewing\".\n let concretelySelectedOrganizationId: string | null = null\n let allowNavigation = true\n\n try {\n const { organizationId, scope, allowedOrganizationIds } = await resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId: selectedOrganizationId,\n tenantId: selectedTenantId,\n })\n scopedOrganizationId = organizationId\n scopedTenantId = scope.tenantId ?? auth.tenantId ?? null\n concretelySelectedOrganizationId = scope.selectedId ?? null\n if (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length === 0) {\n allowNavigation = false\n }\n } catch {\n scopedOrganizationId = auth.orgId ?? null\n scopedTenantId = auth.tenantId ?? null\n concretelySelectedOrganizationId = null\n }\n\n const grantedFeatures = allowNavigation\n ? await rbac.getEffectiveFeatures(auth.sub, {\n tenantId: scopedTenantId,\n organizationId: scopedOrganizationId,\n })\n : []\n const featureChecker = async (features: string[]): Promise<string[]> => {\n if (!allowNavigation || !features.length) return []\n const context = {\n tenantId: scopedTenantId ?? auth.tenantId ?? null,\n organizationId: scopedOrganizationId ?? null,\n }\n const hasAll = await rbac.userHasAllFeatures(auth.sub, features, context)\n if (hasAll) return features\n\n const granted: string[] = []\n for (const feature of features) {\n const hasFeature = await rbac.userHasAllFeatures(auth.sub, [feature], context)\n if (hasFeature) granted.push(feature)\n }\n return granted\n }\n\n let userEntities: Array<{ entityId: string; label: string; href: string }> = []\n if (allowNavigation) {\n try {\n const where: FilterQuery<CustomEntity> = {\n isActive: true,\n showInSidebar: true,\n }\n where.$and = [\n { $or: [{ organizationId: scopedOrganizationId ?? undefined }, { organizationId: null }] },\n { $or: [{ tenantId: scopedTenantId ?? undefined }, { tenantId: null }] },\n ]\n const entities = await em.find(CustomEntity, where, { orderBy: { label: 'asc' } })\n userEntities = entities.map((entity) => ({\n entityId: entity.entityId,\n label: entity.label,\n href: `/backend/entities/user/${encodeURIComponent(entity.entityId)}/records`,\n }))\n } catch {\n userEntities = []\n }\n }\n\n const ctxAuth = {\n roles: auth.roles || [],\n sub: auth.sub,\n tenantId: scopedTenantId,\n orgId: scopedOrganizationId,\n }\n const entries = allowNavigation\n ? await buildAdminNav(\n modules,\n { auth: ctxAuth },\n userEntities,\n translate,\n { checkFeatures: featureChecker },\n )\n : []\n\n let rolePreference: SidebarPreferencesSettings | null = null\n let userPreference: SidebarPreferencesSettings | null = null\n\n if (Array.isArray(auth.roles) && auth.roles.length > 0) {\n const roleRecords = scopedTenantId\n ? await em.find(Role, {\n name: { $in: auth.roles },\n tenantId: scopedTenantId,\n })\n : []\n const roleIds = Array.isArray(roleRecords) ? roleRecords.map((role) => role.id) : []\n if (roleIds.length > 0) {\n rolePreference = await loadFirstRoleSidebarPreference(em, {\n roleIds,\n tenantId: scopedTenantId,\n locale,\n })\n }\n }\n\n const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub\n if (effectiveUserId) {\n userPreference = await findSidebarPreference(em, {\n userId: effectiveUserId,\n tenantId: scopedTenantId,\n organizationId: scopedOrganizationId,\n locale,\n })\n }\n\n const baseGroups = await groupEntries(entries)\n const groupsWithRole = rolePreference\n ? applySidebarPreference<NavGroupWithWeight>(baseGroups, rolePreference)\n : baseGroups\n const baseForUser = adoptSidebarDefaults(groupsWithRole)\n const appliedGroups = userPreference\n ? applySidebarPreference<NavGroupWithWeight>(baseForUser, userPreference)\n : baseForUser\n\n const settingsSections = await serializeSectionGroups(\n convertToSectionNavGroups(\n buildSettingsSections(entries, settingsSectionOrder),\n translate,\n ),\n )\n\n const requestOrganizationId = request ? getSelectedOrganizationFromRequest(request) : null\n const fallbackOrganizationId = selectedOrganizationId ?? requestOrganizationId ?? auth.orgId ?? null\n const brandOrganizationId = scopedOrganizationId\n ?? (fallbackOrganizationId && !isAllOrganizationsSelection(fallbackOrganizationId) ? fallbackOrganizationId : null)\n\n let brand: BackendChromePayload['brand'] = null\n // Resolved here rather than left to callers. `brand` only populates when the organization has a\n // logo, so it is a branding channel, not a dependable \"which organization am I viewing\" source.\n // Without this field every downstream app has to fetch `/api/directory/organization-switcher` and\n // walk its tree for the selected id. The row is already loaded below, so the name costs nothing.\n let currentOrganization: BackendChromePayload['currentOrganization'] = null\n if (brandOrganizationId && scopedTenantId) {\n try {\n const organization = await findOneWithDecryption(\n em,\n Organization,\n { id: brandOrganizationId, tenant: scopedTenantId, deletedAt: null },\n undefined,\n { tenantId: scopedTenantId, organizationId: brandOrganizationId },\n )\n // Only when a concrete organization was selected. Under an all-organizations view\n // `brandOrganizationId` still resolves (to the caller's own organization, which is what keeps\n // branding working), so gating on the loaded row alone would misreport the scope.\n if (organization && concretelySelectedOrganizationId === brandOrganizationId) {\n currentOrganization = { id: String(organization.id), name: organization.name }\n }\n if (organization?.logoUrl) {\n brand = {\n name: organization.name,\n logo: {\n src: organization.logoUrl,\n alt: `${organization.name} logo`,\n preserveAspectRatio: !!organization.logoPreserveAspectRatio,\n },\n }\n }\n } catch {\n // Fail soft, as before: a failed organization lookup must not take down the nav payload.\n brand = null\n currentOrganization = null\n }\n }\n\n return {\n groups: appliedGroups.map(({ weight: _weight, ...group }) => group),\n settingsSections,\n settingsPathPrefixes: computeSettingsPathPrefixes(buildSettingsSections(entries, settingsSectionOrder)),\n profileSections: await serializeSectionGroups(profileSections),\n profilePathPrefixes,\n grantedFeatures,\n roles: Array.isArray(auth.roles) ? auth.roles : [],\n brand,\n currentOrganization,\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }