@open-mercato/core 0.6.7-develop.6788.1.c2a1520a30 → 0.6.7-develop.6814.1.0627c7e9f1

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 (394) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
  3. package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
  4. package/dist/generated/entities/customer_user_invitation/index.js +2 -0
  5. package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
  6. package/dist/generated/entities.ids.generated.js +1 -0
  7. package/dist/generated/entities.ids.generated.js.map +2 -2
  8. package/dist/generated/entity-fields-registry.js +2 -0
  9. package/dist/generated/entity-fields-registry.js.map +2 -2
  10. package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
  11. package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
  12. package/dist/modules/business_rules/api/rules/route.js +71 -4
  13. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  14. package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
  15. package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
  16. package/dist/modules/business_rules/components/ActionRow.js +106 -16
  17. package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
  18. package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
  19. package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
  20. package/dist/modules/business_rules/lib/action-executor.js +148 -0
  21. package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
  22. package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
  23. package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
  24. package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
  25. package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
  26. package/dist/modules/business_rules/lib/rule-engine.js +5 -1
  27. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  28. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
  29. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
  30. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
  31. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
  32. package/dist/modules/configs/lib/upgrade-actions.js +12 -0
  33. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  34. package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
  35. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  36. package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
  37. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
  38. package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
  39. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  40. package/dist/modules/customer_accounts/data/entities.js +3 -0
  41. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  42. package/dist/modules/customer_accounts/data/validators.js +1 -0
  43. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  44. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
  45. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
  46. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
  47. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
  48. package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
  49. package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
  50. package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
  51. package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
  52. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
  53. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
  54. package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
  55. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  56. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
  57. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
  58. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
  59. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  60. package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
  61. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  62. package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
  63. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  64. package/dist/modules/customers/api/deals/[id]/route.js +7 -3
  65. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  66. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
  67. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  68. package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
  69. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  70. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
  71. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  72. package/dist/modules/customers/cli.js +17 -7
  73. package/dist/modules/customers/cli.js.map +2 -2
  74. package/dist/modules/customers/commands/deals.js +48 -10
  75. package/dist/modules/customers/commands/deals.js.map +2 -2
  76. package/dist/modules/customers/commands/people.js +2 -0
  77. package/dist/modules/customers/commands/people.js.map +2 -2
  78. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
  79. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  80. package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
  81. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  82. package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
  83. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  84. package/dist/modules/customers/data/entities.js +8 -0
  85. package/dist/modules/customers/data/entities.js.map +2 -2
  86. package/dist/modules/customers/data/validators.js +2 -1
  87. package/dist/modules/customers/data/validators.js.map +2 -2
  88. package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
  89. package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
  90. package/dist/modules/customers/search.js +50 -29
  91. package/dist/modules/customers/search.js.map +2 -2
  92. package/dist/modules/data_sync/api/mappings/[id]/route.js +22 -9
  93. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  94. package/dist/modules/data_sync/api/mappings/route.js +13 -4
  95. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  96. package/dist/modules/data_sync/api/options.js +7 -2
  97. package/dist/modules/data_sync/api/options.js.map +2 -2
  98. package/dist/modules/data_sync/api/run.js +7 -2
  99. package/dist/modules/data_sync/api/run.js.map +2 -2
  100. package/dist/modules/data_sync/api/runs/[id]/cancel.js +8 -3
  101. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  102. package/dist/modules/data_sync/api/runs/[id]/retry.js +7 -2
  103. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  104. package/dist/modules/data_sync/api/runs/[id]/route.js +8 -3
  105. package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
  106. package/dist/modules/data_sync/api/runs.js +7 -2
  107. package/dist/modules/data_sync/api/runs.js.map +2 -2
  108. package/dist/modules/data_sync/api/schedules/[id]/route.js +19 -6
  109. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  110. package/dist/modules/data_sync/api/schedules/route.js +13 -4
  111. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  112. package/dist/modules/data_sync/api/validate.js +9 -4
  113. package/dist/modules/data_sync/api/validate.js.map +2 -2
  114. package/dist/modules/design_system/acl.js +9 -0
  115. package/dist/modules/design_system/acl.js.map +7 -0
  116. package/dist/modules/design_system/backend/design-system/page.js +11 -0
  117. package/dist/modules/design_system/backend/design-system/page.js.map +7 -0
  118. package/dist/modules/design_system/backend/design-system/page.meta.js +15 -0
  119. package/dist/modules/design_system/backend/design-system/page.meta.js.map +7 -0
  120. package/dist/modules/design_system/gallery/components/CodeSnippet.js +52 -0
  121. package/dist/modules/design_system/gallery/components/CodeSnippet.js.map +7 -0
  122. package/dist/modules/design_system/gallery/components/EntryCard.js +63 -0
  123. package/dist/modules/design_system/gallery/components/EntryCard.js.map +7 -0
  124. package/dist/modules/design_system/gallery/components/GalleryShell.js +163 -0
  125. package/dist/modules/design_system/gallery/components/GalleryShell.js.map +7 -0
  126. package/dist/modules/design_system/gallery/components/VariantPreview.js +9 -0
  127. package/dist/modules/design_system/gallery/components/VariantPreview.js.map +7 -0
  128. package/dist/modules/design_system/gallery/entries/banners.js +186 -0
  129. package/dist/modules/design_system/gallery/entries/banners.js.map +7 -0
  130. package/dist/modules/design_system/gallery/entries/buttons.js +361 -0
  131. package/dist/modules/design_system/gallery/entries/buttons.js.map +7 -0
  132. package/dist/modules/design_system/gallery/entries/charts.js +457 -0
  133. package/dist/modules/design_system/gallery/entries/charts.js.map +7 -0
  134. package/dist/modules/design_system/gallery/entries/dates.js +306 -0
  135. package/dist/modules/design_system/gallery/entries/dates.js.map +7 -0
  136. package/dist/modules/design_system/gallery/entries/detail.js +358 -0
  137. package/dist/modules/design_system/gallery/entries/detail.js.map +7 -0
  138. package/dist/modules/design_system/gallery/entries/display.js +785 -0
  139. package/dist/modules/design_system/gallery/entries/display.js.map +7 -0
  140. package/dist/modules/design_system/gallery/entries/feedback.js +658 -0
  141. package/dist/modules/design_system/gallery/entries/feedback.js.map +7 -0
  142. package/dist/modules/design_system/gallery/entries/filters.js +505 -0
  143. package/dist/modules/design_system/gallery/entries/filters.js.map +7 -0
  144. package/dist/modules/design_system/gallery/entries/foundations.js +453 -0
  145. package/dist/modules/design_system/gallery/entries/foundations.js.map +7 -0
  146. package/dist/modules/design_system/gallery/entries/icons.js +140 -0
  147. package/dist/modules/design_system/gallery/entries/icons.js.map +7 -0
  148. package/dist/modules/design_system/gallery/entries/inputs.js +1217 -0
  149. package/dist/modules/design_system/gallery/entries/inputs.js.map +7 -0
  150. package/dist/modules/design_system/gallery/entries/messages.js +334 -0
  151. package/dist/modules/design_system/gallery/entries/messages.js.map +7 -0
  152. package/dist/modules/design_system/gallery/entries/navigation.js +475 -0
  153. package/dist/modules/design_system/gallery/entries/navigation.js.map +7 -0
  154. package/dist/modules/design_system/gallery/entries/notifications.js +257 -0
  155. package/dist/modules/design_system/gallery/entries/notifications.js.map +7 -0
  156. package/dist/modules/design_system/gallery/entries/overlays.js +576 -0
  157. package/dist/modules/design_system/gallery/entries/overlays.js.map +7 -0
  158. package/dist/modules/design_system/gallery/entries/scaffolding.js +378 -0
  159. package/dist/modules/design_system/gallery/entries/scaffolding.js.map +7 -0
  160. package/dist/modules/design_system/gallery/entries/schedule.js +297 -0
  161. package/dist/modules/design_system/gallery/entries/schedule.js.map +7 -0
  162. package/dist/modules/design_system/gallery/registry.js +139 -0
  163. package/dist/modules/design_system/gallery/registry.js.map +7 -0
  164. package/dist/modules/design_system/gallery/types.js +1 -0
  165. package/dist/modules/design_system/gallery/types.js.map +7 -0
  166. package/dist/modules/design_system/index.js +9 -0
  167. package/dist/modules/design_system/index.js.map +7 -0
  168. package/dist/modules/design_system/setup.js +14 -0
  169. package/dist/modules/design_system/setup.js.map +7 -0
  170. package/dist/modules/entities/api/entity-settings.js +59 -9
  171. package/dist/modules/entities/api/entity-settings.js.map +2 -2
  172. package/dist/modules/entities/backend/config/settings/page.js +10 -0
  173. package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
  174. package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
  175. package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
  176. package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
  177. package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
  178. package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
  179. package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
  180. package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
  181. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  182. package/dist/modules/inbox_ops/search.js +15 -4
  183. package/dist/modules/inbox_ops/search.js.map +2 -2
  184. package/dist/modules/integrations/api/[id]/credentials/route.js +11 -5
  185. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  186. package/dist/modules/integrations/api/[id]/health/route.js +6 -3
  187. package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
  188. package/dist/modules/integrations/api/[id]/route.js +6 -3
  189. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  190. package/dist/modules/integrations/api/[id]/state/route.js +6 -3
  191. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  192. package/dist/modules/integrations/api/[id]/version/route.js +6 -3
  193. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  194. package/dist/modules/integrations/api/logs/route.js +6 -3
  195. package/dist/modules/integrations/api/logs/route.js.map +2 -2
  196. package/dist/modules/integrations/api/route.js +6 -3
  197. package/dist/modules/integrations/api/route.js.map +2 -2
  198. package/dist/modules/integrations/api/umes-read.js +2 -2
  199. package/dist/modules/integrations/api/umes-read.js.map +2 -2
  200. package/dist/modules/integrations/lib/organization-scope.js +2 -9
  201. package/dist/modules/integrations/lib/organization-scope.js.map +2 -2
  202. package/dist/modules/messages/search.js +10 -5
  203. package/dist/modules/messages/search.js.map +2 -2
  204. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
  205. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  206. package/dist/modules/payment_gateways/data/entities.js +4 -0
  207. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  208. package/dist/modules/payment_gateways/lib/gateway-service.js +12 -2
  209. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  210. package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
  211. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  212. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
  213. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
  214. package/dist/modules/payment_gateways/setup.js +47 -0
  215. package/dist/modules/payment_gateways/setup.js.map +2 -2
  216. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
  217. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
  218. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
  219. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
  220. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
  221. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
  222. package/dist/modules/portal/lib/navigation.js +7 -0
  223. package/dist/modules/portal/lib/navigation.js.map +7 -0
  224. package/dist/modules/query_index/data/entities.js +2 -1
  225. package/dist/modules/query_index/data/entities.js.map +2 -2
  226. package/dist/modules/query_index/lib/engine.js +75 -50
  227. package/dist/modules/query_index/lib/engine.js.map +2 -2
  228. package/dist/modules/query_index/lib/search-tokens.js +15 -5
  229. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  230. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js +17 -0
  231. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js.map +7 -0
  232. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
  233. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
  234. package/generated/entities/customer_deal_person_link/index.ts +1 -0
  235. package/generated/entities/customer_user_invitation/index.ts +1 -0
  236. package/generated/entities.ids.generated.ts +1 -0
  237. package/generated/entity-fields-registry.ts +2 -0
  238. package/package.json +16 -16
  239. package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
  240. package/src/modules/business_rules/api/rules/route.ts +92 -4
  241. package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
  242. package/src/modules/business_rules/components/ActionRow.tsx +156 -18
  243. package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
  244. package/src/modules/business_rules/i18n/de.json +12 -0
  245. package/src/modules/business_rules/i18n/en.json +12 -0
  246. package/src/modules/business_rules/i18n/es.json +12 -0
  247. package/src/modules/business_rules/i18n/pl.json +12 -0
  248. package/src/modules/business_rules/lib/action-executor.ts +198 -0
  249. package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
  250. package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
  251. package/src/modules/business_rules/lib/rule-engine.ts +4 -0
  252. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
  253. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
  254. package/src/modules/configs/lib/upgrade-actions.ts +14 -0
  255. package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
  256. package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
  257. package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
  258. package/src/modules/customer_accounts/data/entities.ts +3 -0
  259. package/src/modules/customer_accounts/data/validators.ts +1 -0
  260. package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
  261. package/src/modules/customer_accounts/i18n/de.json +6 -0
  262. package/src/modules/customer_accounts/i18n/en.json +6 -0
  263. package/src/modules/customer_accounts/i18n/es.json +6 -0
  264. package/src/modules/customer_accounts/i18n/pl.json +6 -0
  265. package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
  266. package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
  267. package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
  268. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
  269. package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
  270. package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
  271. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
  272. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
  273. package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
  274. package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
  275. package/src/modules/customers/api/deals/[id]/route.ts +10 -2
  276. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
  277. package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
  278. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
  279. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
  280. package/src/modules/customers/cli.ts +17 -7
  281. package/src/modules/customers/commands/deals.ts +50 -9
  282. package/src/modules/customers/commands/people.ts +3 -0
  283. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
  284. package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
  285. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
  286. package/src/modules/customers/data/entities.ts +9 -1
  287. package/src/modules/customers/data/validators.ts +1 -0
  288. package/src/modules/customers/i18n/de.json +18 -0
  289. package/src/modules/customers/i18n/en.json +18 -0
  290. package/src/modules/customers/i18n/es.json +18 -0
  291. package/src/modules/customers/i18n/pl.json +18 -0
  292. package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
  293. package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
  294. package/src/modules/customers/search.ts +59 -27
  295. package/src/modules/data_sync/AGENTS.md +1 -0
  296. package/src/modules/data_sync/api/mappings/[id]/route.ts +22 -9
  297. package/src/modules/data_sync/api/mappings/route.ts +13 -4
  298. package/src/modules/data_sync/api/options.ts +7 -2
  299. package/src/modules/data_sync/api/run.ts +7 -2
  300. package/src/modules/data_sync/api/runs/[id]/cancel.ts +8 -3
  301. package/src/modules/data_sync/api/runs/[id]/retry.ts +7 -2
  302. package/src/modules/data_sync/api/runs/[id]/route.ts +8 -3
  303. package/src/modules/data_sync/api/runs.ts +7 -2
  304. package/src/modules/data_sync/api/schedules/[id]/route.ts +19 -6
  305. package/src/modules/data_sync/api/schedules/route.ts +13 -4
  306. package/src/modules/data_sync/api/validate.ts +9 -4
  307. package/src/modules/design_system/AGENTS.md +34 -0
  308. package/src/modules/design_system/acl.ts +5 -0
  309. package/src/modules/design_system/backend/design-system/page.meta.ts +11 -0
  310. package/src/modules/design_system/backend/design-system/page.tsx +14 -0
  311. package/src/modules/design_system/gallery/components/CodeSnippet.tsx +55 -0
  312. package/src/modules/design_system/gallery/components/EntryCard.tsx +95 -0
  313. package/src/modules/design_system/gallery/components/GalleryShell.tsx +206 -0
  314. package/src/modules/design_system/gallery/components/VariantPreview.tsx +15 -0
  315. package/src/modules/design_system/gallery/entries/banners.tsx +226 -0
  316. package/src/modules/design_system/gallery/entries/buttons.tsx +422 -0
  317. package/src/modules/design_system/gallery/entries/charts.tsx +518 -0
  318. package/src/modules/design_system/gallery/entries/dates.tsx +325 -0
  319. package/src/modules/design_system/gallery/entries/detail.tsx +403 -0
  320. package/src/modules/design_system/gallery/entries/display.tsx +893 -0
  321. package/src/modules/design_system/gallery/entries/feedback.tsx +746 -0
  322. package/src/modules/design_system/gallery/entries/filters.tsx +570 -0
  323. package/src/modules/design_system/gallery/entries/foundations.tsx +529 -0
  324. package/src/modules/design_system/gallery/entries/icons.tsx +168 -0
  325. package/src/modules/design_system/gallery/entries/inputs.tsx +1516 -0
  326. package/src/modules/design_system/gallery/entries/messages.tsx +339 -0
  327. package/src/modules/design_system/gallery/entries/navigation.tsx +573 -0
  328. package/src/modules/design_system/gallery/entries/notifications.tsx +285 -0
  329. package/src/modules/design_system/gallery/entries/overlays.tsx +677 -0
  330. package/src/modules/design_system/gallery/entries/scaffolding.tsx +430 -0
  331. package/src/modules/design_system/gallery/entries/schedule.tsx +307 -0
  332. package/src/modules/design_system/gallery/registry.ts +152 -0
  333. package/src/modules/design_system/gallery/types.ts +32 -0
  334. package/src/modules/design_system/i18n/.hardcoded-allowlist.json +71 -0
  335. package/src/modules/design_system/i18n/de.json +46 -0
  336. package/src/modules/design_system/i18n/en.json +46 -0
  337. package/src/modules/design_system/i18n/es.json +46 -0
  338. package/src/modules/design_system/i18n/pl.json +46 -0
  339. package/src/modules/design_system/index.ts +7 -0
  340. package/src/modules/design_system/setup.ts +12 -0
  341. package/src/modules/entities/api/entity-settings.ts +66 -22
  342. package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
  343. package/src/modules/entities/backend/config/settings/page.tsx +12 -0
  344. package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
  345. package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
  346. package/src/modules/entities/i18n/de.json +9 -0
  347. package/src/modules/entities/i18n/en.json +9 -0
  348. package/src/modules/entities/i18n/es.json +9 -0
  349. package/src/modules/entities/i18n/pl.json +9 -0
  350. package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
  351. package/src/modules/inbox_ops/i18n/de.json +3 -0
  352. package/src/modules/inbox_ops/i18n/en.json +3 -0
  353. package/src/modules/inbox_ops/i18n/es.json +3 -0
  354. package/src/modules/inbox_ops/i18n/pl.json +3 -0
  355. package/src/modules/inbox_ops/search.ts +20 -4
  356. package/src/modules/integrations/api/[id]/credentials/route.ts +11 -5
  357. package/src/modules/integrations/api/[id]/health/route.ts +6 -3
  358. package/src/modules/integrations/api/[id]/route.ts +6 -3
  359. package/src/modules/integrations/api/[id]/state/route.ts +6 -3
  360. package/src/modules/integrations/api/[id]/version/route.ts +6 -3
  361. package/src/modules/integrations/api/logs/route.ts +6 -3
  362. package/src/modules/integrations/api/route.ts +6 -3
  363. package/src/modules/integrations/api/umes-read.ts +2 -2
  364. package/src/modules/integrations/lib/organization-scope.ts +5 -25
  365. package/src/modules/messages/i18n/de.json +2 -0
  366. package/src/modules/messages/i18n/en.json +2 -0
  367. package/src/modules/messages/i18n/es.json +2 -0
  368. package/src/modules/messages/i18n/pl.json +2 -0
  369. package/src/modules/messages/search.ts +11 -5
  370. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
  371. package/src/modules/payment_gateways/data/entities.ts +5 -0
  372. package/src/modules/payment_gateways/i18n/de.json +5 -1
  373. package/src/modules/payment_gateways/i18n/en.json +5 -1
  374. package/src/modules/payment_gateways/i18n/es.json +5 -1
  375. package/src/modules/payment_gateways/i18n/pl.json +5 -1
  376. package/src/modules/payment_gateways/lib/gateway-service.ts +12 -2
  377. package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
  378. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
  379. package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
  380. package/src/modules/payment_gateways/setup.ts +62 -0
  381. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
  382. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
  383. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
  384. package/src/modules/portal/i18n/de.json +18 -0
  385. package/src/modules/portal/i18n/en.json +18 -0
  386. package/src/modules/portal/i18n/es.json +18 -0
  387. package/src/modules/portal/i18n/pl.json +18 -0
  388. package/src/modules/portal/lib/navigation.ts +3 -0
  389. package/src/modules/query_index/data/entities.ts +7 -0
  390. package/src/modules/query_index/lib/engine.ts +92 -63
  391. package/src/modules/query_index/lib/search-tokens.ts +16 -4
  392. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +12 -0
  393. package/src/modules/query_index/migrations/Migration20260731105052_query_index.ts +35 -0
  394. package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/api/entity-settings.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\n// Tenant-scoped policy: when true, new custom entities are created with\n// `access_restricted = true` unless the create request explicitly sets the flag.\nconst CONFIG_MODULE = 'entities'\nconst NEW_ENTITIES_RESTRICTED_KEY = 'newEntitiesRestrictedByDefault'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.definitions.view'] },\n PUT: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\ntype ModuleConfigServiceLike = {\n getValue: (\n moduleId: string,\n name: string,\n options?: { defaultValue?: unknown; scope?: { tenantId?: string | null } },\n ) => Promise<unknown>\n setValue: (\n moduleId: string,\n name: string,\n value: unknown,\n scope?: { tenantId?: string | null },\n ) => Promise<unknown>\n}\n\nasync function readPolicy(tenantId: string | null): Promise<boolean> {\n try {\n const { resolve } = await createRequestContainer()\n const moduleConfigService = resolve('moduleConfigService') as ModuleConfigServiceLike\n const value = await moduleConfigService.getValue(CONFIG_MODULE, NEW_ENTITIES_RESTRICTED_KEY, {\n defaultValue: false,\n scope: { tenantId },\n })\n return value === true\n } catch {\n return false\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const newEntitiesRestrictedByDefault = await readPolicy(auth.tenantId ?? null)\n return NextResponse.json({ newEntitiesRestrictedByDefault })\n}\n\nconst putBodySchema = z.object({\n newEntitiesRestrictedByDefault: z.boolean(),\n})\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: unknown\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = putBodySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n }\n const { resolve } = await createRequestContainer()\n const moduleConfigService = resolve('moduleConfigService') as ModuleConfigServiceLike\n await moduleConfigService.setValue(\n CONFIG_MODULE,\n NEW_ENTITIES_RESTRICTED_KEY,\n parsed.data.newEntitiesRestrictedByDefault,\n { tenantId: auth.tenantId ?? null },\n )\n return NextResponse.json({ ok: true, newEntitiesRestrictedByDefault: parsed.data.newEntitiesRestrictedByDefault })\n}\n\nconst settingsResponseSchema = z.object({\n newEntitiesRestrictedByDefault: z.boolean(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Custom entity workspace settings',\n methods: {\n GET: {\n summary: 'Get custom entity settings',\n description: 'Returns the tenant-scoped default-restricted policy for new custom entities.',\n responses: [\n { status: 200, description: 'Current settings', schema: settingsResponseSchema },\n { status: 401, description: 'Missing authentication', schema: z.object({ error: z.string() }) },\n ],\n },\n PUT: {\n summary: 'Update custom entity settings',\n description: 'Sets the tenant-scoped default-restricted policy for new custom entities.',\n requestBody: { schema: putBodySchema },\n responses: [\n { status: 200, description: 'Updated settings', schema: z.object({ ok: z.boolean(), newEntitiesRestrictedByDefault: z.boolean() }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Missing authentication', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { beginEntitiesMutationGuard } from './definitions.mutation-guard'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { ModuleConfigService } from '../../configs/lib/module-config-service'\n\n// Tenant-scoped policy: when true, new custom entities are created with\n// `access_restricted = true` unless the create request explicitly sets the flag.\nconst CONFIG_MODULE = 'entities'\nconst NEW_ENTITIES_RESTRICTED_KEY = 'newEntitiesRestrictedByDefault'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.definitions.view'] },\n PUT: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nasync function readPolicy(tenantId: string | null): Promise<boolean> {\n try {\n const { resolve } = await createRequestContainer()\n const moduleConfigService = resolve('moduleConfigService') as ModuleConfigService\n const value = await moduleConfigService.getValue(CONFIG_MODULE, NEW_ENTITIES_RESTRICTED_KEY, {\n defaultValue: false,\n scope: { tenantId },\n })\n return value === true\n } catch {\n return false\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const { resolve } = await createRequestContainer()\n const moduleConfigService = resolve('moduleConfigService') as ModuleConfigService\n const record = await moduleConfigService.getRecord(CONFIG_MODULE, NEW_ENTITIES_RESTRICTED_KEY, {\n tenantId: auth.tenantId ?? null,\n })\n return NextResponse.json({\n newEntitiesRestrictedByDefault: record ? record.value === true : false,\n updatedAt: record ? record.updatedAt : null,\n })\n}\n\nconst putBodySchema = z.object({\n newEntitiesRestrictedByDefault: z.boolean(),\n expectedUpdatedAt: z.string().optional(),\n})\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: unknown\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = putBodySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const moduleConfigService = resolve('moduleConfigService') as ModuleConfigService\n\n const currentRecord = await moduleConfigService.getRecord(\n CONFIG_MODULE,\n NEW_ENTITIES_RESTRICTED_KEY,\n { tenantId: auth.tenantId ?? null },\n )\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: 'entities.settings',\n resourceId: NEW_ENTITIES_RESTRICTED_KEY,\n operation: currentRecord ? 'update' : 'create',\n mutationPayload: parsed.data as unknown as Record<string, unknown>,\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n const ifMatchHeader = req.headers.get('If-Match') || req.headers.get('x-om-ext-optimistic-lock-expected-updated-at')\n const expectedUpdatedAt = ifMatchHeader || parsed.data.expectedUpdatedAt\n\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'entities.settings',\n resourceId: NEW_ENTITIES_RESTRICTED_KEY,\n current: currentRecord ? currentRecord.updatedAt : null,\n expected: expectedUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n const updatedRecord = await moduleConfigService.setValue(\n CONFIG_MODULE,\n NEW_ENTITIES_RESTRICTED_KEY,\n parsed.data.newEntitiesRestrictedByDefault,\n { tenantId: auth.tenantId ?? null },\n )\n\n await guard.runAfterSuccess()\n\n return NextResponse.json({\n ok: true,\n newEntitiesRestrictedByDefault: parsed.data.newEntitiesRestrictedByDefault,\n updatedAt: updatedRecord ? updatedRecord.updatedAt : null,\n })\n}\n\nconst settingsResponseSchema = z.object({\n newEntitiesRestrictedByDefault: z.boolean(),\n updatedAt: z.string().nullable().optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Custom entity workspace settings',\n methods: {\n GET: {\n summary: 'Get custom entity settings',\n description: 'Returns the tenant-scoped default-restricted policy for new custom entities.',\n responses: [\n { status: 200, description: 'Current settings', schema: settingsResponseSchema },\n { status: 401, description: 'Missing authentication', schema: z.object({ error: z.string() }) },\n ],\n },\n PUT: {\n summary: 'Update custom entity settings',\n description: 'Sets the tenant-scoped default-restricted policy for new custom entities.',\n requestBody: { schema: putBodySchema },\n responses: [\n { status: 200, description: 'Updated settings', schema: z.object({ ok: z.boolean(), newEntitiesRestrictedByDefault: z.boolean(), updatedAt: z.string().nullable().optional() }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Missing authentication', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Optimistic lock conflict', schema: z.object({ code: z.string(), error: z.string() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,10 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { Page, PageBody } from "@open-mercato/ui/backend/Page";
3
+ import { EntitySettingsManager } from "../../../components/EntitySettingsManager.js";
4
+ function EntitySettingsPage() {
5
+ return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { className: "space-y-8", children: /* @__PURE__ */ jsx(EntitySettingsManager, {}) }) });
6
+ }
7
+ export {
8
+ EntitySettingsPage as default
9
+ };
10
+ //# sourceMappingURL=page.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/entities/backend/config/settings/page.tsx"],
4
+ "sourcesContent": ["import { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { EntitySettingsManager } from '../../../components/EntitySettingsManager'\n\nexport default function EntitySettingsPage() {\n return (\n <Page>\n <PageBody className=\"space-y-8\">\n <EntitySettingsManager />\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "AAOQ;AAPR,SAAS,MAAM,gBAAgB;AAC/B,SAAS,6BAA6B;AAEvB,SAAR,qBAAsC;AAC3C,SACE,oBAAC,QACC,8BAAC,YAAS,WAAU,aAClB,8BAAC,yBAAsB,GACzB,GACF;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ const settingsIcon = React.createElement(
3
+ "svg",
4
+ {
5
+ width: 16,
6
+ height: 16,
7
+ viewBox: "0 0 24 24",
8
+ fill: "none",
9
+ stroke: "currentColor",
10
+ strokeWidth: 2,
11
+ strokeLinecap: "round",
12
+ strokeLinejoin: "round"
13
+ },
14
+ React.createElement("circle", { cx: 12, cy: 12, r: 3 }),
15
+ React.createElement("path", {
16
+ d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"
17
+ })
18
+ );
19
+ const metadata = {
20
+ requireAuth: true,
21
+ requireFeatures: ["entities.definitions.manage"],
22
+ pageTitle: "Custom Entities",
23
+ pageTitleKey: "entities.settings.title",
24
+ pageGroup: "Module Configs",
25
+ pageGroupKey: "settings.sections.moduleConfigs",
26
+ pageOrder: 5,
27
+ icon: settingsIcon,
28
+ pageContext: "settings",
29
+ breadcrumb: [
30
+ { label: "Custom Entities", labelKey: "entities.settings.title" }
31
+ ]
32
+ };
33
+ export {
34
+ metadata
35
+ };
36
+ //# sourceMappingURL=page.meta.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/entities/backend/config/settings/page.meta.ts"],
4
+ "sourcesContent": ["import React from 'react'\n\nconst settingsIcon = React.createElement(\n 'svg',\n {\n width: 16,\n height: 16,\n viewBox: '0 0 24 24',\n fill: 'none',\n stroke: 'currentColor',\n strokeWidth: 2,\n strokeLinecap: 'round',\n strokeLinejoin: 'round',\n },\n React.createElement('circle', { cx: 12, cy: 12, r: 3 }),\n React.createElement('path', {\n d: 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'\n })\n)\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['entities.definitions.manage'],\n pageTitle: 'Custom Entities',\n pageTitleKey: 'entities.settings.title',\n pageGroup: 'Module Configs',\n pageGroupKey: 'settings.sections.moduleConfigs',\n pageOrder: 5,\n icon: settingsIcon,\n pageContext: 'settings' as const,\n breadcrumb: [\n { label: 'Custom Entities', labelKey: 'entities.settings.title' },\n ],\n} as const\n"],
5
+ "mappings": "AAAA,OAAO,WAAW;AAElB,MAAM,eAAe,MAAM;AAAA,EACzB;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,eAAe;AAAA,IACf,gBAAgB;AAAA,EAClB;AAAA,EACA,MAAM,cAAc,UAAU,EAAE,IAAI,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC;AAAA,EACtD,MAAM,cAAc,QAAQ;AAAA,IAC1B,GAAG;AAAA,EACL,CAAC;AACH;AAEO,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,6BAA6B;AAAA,EAC/C,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,MAAM;AAAA,EACN,aAAa;AAAA,EACb,YAAY;AAAA,IACV,EAAE,OAAO,mBAAmB,UAAU,0BAA0B;AAAA,EAClE;AACF;",
6
+ "names": []
7
+ }
@@ -10,6 +10,7 @@ import { useRouter } from "next/navigation";
10
10
  import { pushWithFlash } from "@open-mercato/ui/backend/utils/flash";
11
11
  import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
12
12
  import { LoadingMessage } from "@open-mercato/ui/backend/detail";
13
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
13
14
  const schema = upsertCustomEntitySchema;
14
15
  import { Page, PageBody } from "@open-mercato/ui/backend/Page";
15
16
  function CreateEntityPage() {
@@ -19,17 +20,25 @@ function CreateEntityPage() {
19
20
  React.useEffect(() => {
20
21
  let active = true;
21
22
  (async () => {
22
- const data = await readApiResultOrThrow(
23
- "/api/entities/entity-settings",
24
- void 0,
25
- { errorMessage: "[internal] Failed to load entity settings", fallback: { newEntitiesRestrictedByDefault: false } }
26
- ).catch(() => ({ newEntitiesRestrictedByDefault: false }));
27
- if (active) setDefaultRestricted(data?.newEntitiesRestrictedByDefault === true);
23
+ try {
24
+ const data = await readApiResultOrThrow(
25
+ "/api/entities/entity-settings",
26
+ void 0,
27
+ {
28
+ errorMessage: t("entities.userEntities.errors.settingsLoadFailed", "Could not load default restriction policy; using default"),
29
+ fallback: { newEntitiesRestrictedByDefault: false }
30
+ }
31
+ );
32
+ if (active) setDefaultRestricted(data?.newEntitiesRestrictedByDefault === true);
33
+ } catch {
34
+ flash(t("entities.userEntities.errors.settingsLoadFailed", "Could not load default restriction policy; using default"), "warning");
35
+ if (active) setDefaultRestricted(false);
36
+ }
28
37
  })();
29
38
  return () => {
30
39
  active = false;
31
40
  };
32
- }, []);
41
+ }, [t]);
33
42
  const fields = React.useMemo(() => [
34
43
  {
35
44
  id: "entityId",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/entities/backend/entities/user/create/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { useRouter } from 'next/navigation'\nimport { pushWithFlash } from '@open-mercato/ui/backend/utils/flash'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\n\nconst schema = upsertCustomEntitySchema\n\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\n\nexport default function CreateEntityPage() {\n const t = useT()\n const router = useRouter()\n // Pre-fill the restriction toggle from the tenant default-restricted policy so\n // the form reflects the workspace posture; null while loading.\n const [defaultRestricted, setDefaultRestricted] = React.useState<boolean | null>(null)\n React.useEffect(() => {\n let active = true\n ;(async () => {\n const data = await readApiResultOrThrow<{ newEntitiesRestrictedByDefault?: boolean }>(\n '/api/entities/entity-settings',\n undefined,\n { errorMessage: '[internal] Failed to load entity settings', fallback: { newEntitiesRestrictedByDefault: false } },\n ).catch(() => ({ newEntitiesRestrictedByDefault: false }))\n if (active) setDefaultRestricted(data?.newEntitiesRestrictedByDefault === true)\n })()\n return () => { active = false }\n }, [])\n const fields = React.useMemo<CrudField[]>(() => ([\n {\n id: 'entityId',\n label: t('entities.userEntities.form.entityId.label', 'Entity ID'),\n type: 'text',\n required: true,\n placeholder: t('entities.userEntities.form.entityId.placeholder', 'module_name:entity_id'),\n },\n { id: 'label', label: t('entities.userEntities.form.label.label', 'Label'), type: 'text', required: true },\n { id: 'description', label: t('entities.userEntities.form.description.label', 'Description'), type: 'textarea' },\n {\n id: 'defaultEditor',\n label: t('entities.userEntities.form.defaultEditor.label', 'Default Editor (multiline)'),\n type: 'select',\n options: [\n { value: '', label: t('entities.userEntities.form.defaultEditor.options.default', 'Default (Markdown)') },\n { value: 'markdown', label: t('entities.userEntities.form.defaultEditor.options.markdown', 'Markdown (UIW)') },\n { value: 'simpleMarkdown', label: t('entities.userEntities.form.defaultEditor.options.simpleMarkdown', 'Simple Markdown') },\n { value: 'htmlRichText', label: t('entities.userEntities.form.defaultEditor.options.htmlRichText', 'HTML Rich Text') },\n { value: 'plain', label: t('entities.userEntities.form.defaultEditor.options.plain', 'Plain textarea') },\n ],\n } as unknown as CrudField,\n { id: 'showInSidebar', label: t('entities.userEntities.form.showInSidebar.label', 'Show in sidebar'), type: 'checkbox' } as CrudField,\n {\n id: 'accessRestricted',\n label: t('entities.userEntities.form.accessRestricted.label', 'Restrict record access'),\n type: 'checkbox',\n description: t('entities.userEntities.form.accessRestricted.help', 'Require an explicit per-entity permission to view or manage this entity\u2019s records. Leave off to allow anyone with the general records permission.'),\n } as CrudField,\n ]), [t])\n\n if (defaultRestricted === null) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('entities.userEntities.form.loading', 'Loading\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={t('entities.userEntities.form.title', 'Create Entity')}\n backHref=\"/backend/entities/user\"\n schema={schema}\n fields={fields}\n initialValues={{ entityId: 'user:your_entity', label: '', showInSidebar: false, accessRestricted: defaultRestricted }}\n submitLabel={t('entities.userEntities.form.submit', 'Create')}\n cancelHref=\"/backend/entities/user\"\n onSubmit={async (vals) => {\n const entityId = await submitCreateEntity({\n values: vals as Record<string, unknown>,\n messages: {\n entityIdRequired: t('entities.userEntities.errors.entityIdRequired', 'Entity ID is required'),\n entityIdExists: t('entities.userEntities.errors.entityIdExists', 'Entity ID already exists'),\n loadFailed: t('entities.userEntities.errors.loadFailed', 'Failed to load entities'),\n },\n })\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {}\n const successMessage = t('entities.userEntities.flash.created', 'Entity created')\n pushWithFlash(router, `/backend/entities/user/${encodeURIComponent(entityId)}`, successMessage, 'success')\n }}\n />\n </PageBody>\n </Page>\n )\n}\n\ntype EntityListEntry = {\n entityId?: string\n source?: string\n}\n\ntype FetchCustomEntities = (errorMessage?: string) => Promise<EntityListEntry[]>\n\nasync function defaultFetchCustomEntities(errorMessage?: string): Promise<EntityListEntry[]> {\n const data = await readApiResultOrThrow<{ items?: EntityListEntry[] }>(\n '/api/entities/entities',\n undefined,\n { errorMessage: errorMessage ?? 'Failed to load entities', fallback: { items: [] } },\n )\n return Array.isArray(data?.items) ? data.items! : []\n}\n\ntype CreateEntityRequest = (payload: Record<string, unknown>) => Promise<void>\n\nasync function defaultCreateEntityRequest(payload: Record<string, unknown>) {\n await createCrud('entities/entities', payload)\n}\n\nexport async function submitCreateEntity(options: {\n values: Record<string, unknown>\n fetchEntities?: FetchCustomEntities\n createEntity?: CreateEntityRequest\n messages?: {\n entityIdRequired?: string\n entityIdExists?: string\n loadFailed?: string\n }\n}): Promise<string> {\n const {\n values,\n fetchEntities = defaultFetchCustomEntities,\n createEntity = defaultCreateEntityRequest,\n messages,\n } = options\n const rawEntityId = typeof values.entityId === 'string' ? values.entityId.trim() : ''\n if (!rawEntityId) {\n const message = messages?.entityIdRequired ?? 'Entity ID is required'\n throw createCrudFormError(message, { entityId: message })\n }\n\n const existing = await fetchEntities(messages?.loadFailed)\n const exists = existing.some(\n (entry) => entry?.entityId === rawEntityId && (entry?.source === 'custom' || entry?.source === undefined),\n )\n if (exists) {\n const message = messages?.entityIdExists ?? 'Entity ID already exists'\n throw createCrudFormError(message, { entityId: message })\n }\n\n const payload: Record<string, unknown> = {\n ...values,\n entityId: rawEntityId,\n labelField: 'name',\n defaultEditor:\n typeof (values as Record<string, unknown>).defaultEditor === 'string' &&\n (values as Record<string, unknown>).defaultEditor\n ? (values as Record<string, unknown>).defaultEditor\n : undefined,\n }\n\n await createEntity(payload)\n return rawEntityId\n}\n"],
5
- "mappings": ";AAsEU;AArEV,YAAY,WAAW;AAEvB,SAAS,YAAY;AACrB,SAAS,gBAAgC;AACzC,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AACpC,SAAS,gCAAgC;AACzC,SAAS,iBAAiB;AAC1B,SAAS,qBAAqB;AAC9B,SAAS,4BAA4B;AACrC,SAAS,sBAAsB;AAE/B,MAAM,SAAS;AAEf,SAAS,MAAM,gBAAgB;AAEhB,SAAR,mBAAoC;AACzC,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AAGzB,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAyB,IAAI;AACrF,QAAM,UAAU,MAAM;AACpB,QAAI,SAAS;AACZ,KAAC,YAAY;AACZ,YAAM,OAAO,MAAM;AAAA,QACjB;AAAA,QACA;AAAA,QACA,EAAE,cAAc,6CAA6C,UAAU,EAAE,gCAAgC,MAAM,EAAE;AAAA,MACnH,EAAE,MAAM,OAAO,EAAE,gCAAgC,MAAM,EAAE;AACzD,UAAI,OAAQ,sBAAqB,MAAM,mCAAmC,IAAI;AAAA,IAChF,GAAG;AACH,WAAO,MAAM;AAAE,eAAS;AAAA,IAAM;AAAA,EAChC,GAAG,CAAC,CAAC;AACL,QAAM,SAAS,MAAM,QAAqB,MAAO;AAAA,IAC/C;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,6CAA6C,WAAW;AAAA,MACjE,MAAM;AAAA,MACN,UAAU;AAAA,MACV,aAAa,EAAE,mDAAmD,uBAAuB;AAAA,IAC3F;AAAA,IACA,EAAE,IAAI,SAAS,OAAO,EAAE,0CAA0C,OAAO,GAAG,MAAM,QAAQ,UAAU,KAAK;AAAA,IACzG,EAAE,IAAI,eAAe,OAAO,EAAE,gDAAgD,aAAa,GAAG,MAAM,WAAW;AAAA,IAC/G;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,kDAAkD,4BAA4B;AAAA,MACvF,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,OAAO,IAAI,OAAO,EAAE,4DAA4D,oBAAoB,EAAE;AAAA,QACxG,EAAE,OAAO,YAAY,OAAO,EAAE,6DAA6D,gBAAgB,EAAE;AAAA,QAC7G,EAAE,OAAO,kBAAkB,OAAO,EAAE,mEAAmE,iBAAiB,EAAE;AAAA,QAC1H,EAAE,OAAO,gBAAgB,OAAO,EAAE,iEAAiE,gBAAgB,EAAE;AAAA,QACrH,EAAE,OAAO,SAAS,OAAO,EAAE,0DAA0D,gBAAgB,EAAE;AAAA,MACzG;AAAA,IACF;AAAA,IACA,EAAE,IAAI,iBAAiB,OAAO,EAAE,kDAAkD,iBAAiB,GAAG,MAAM,WAAW;AAAA,IACvH;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,qDAAqD,wBAAwB;AAAA,MACtF,MAAM;AAAA,MACN,aAAa,EAAE,oDAAoD,wJAAmJ;AAAA,IACxN;AAAA,EACF,GAAI,CAAC,CAAC,CAAC;AAEP,MAAI,sBAAsB,MAAM;AAC9B,WACE,oBAAC,QACC,8BAAC,YACC,8BAAC,kBAAe,OAAO,EAAE,sCAAsC,eAAU,GAAG,GAC9E,GACF;AAAA,EAEJ;AAEA,SACE,oBAAC,QACC,8BAAC,YACC;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,EAAE,oCAAoC,eAAe;AAAA,MAC5D,UAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,eAAe,EAAE,UAAU,oBAAoB,OAAO,IAAI,eAAe,OAAO,kBAAkB,kBAAkB;AAAA,MACpH,aAAa,EAAE,qCAAqC,QAAQ;AAAA,MAC5D,YAAW;AAAA,MACX,UAAU,OAAO,SAAS;AACxB,cAAM,WAAW,MAAM,mBAAmB;AAAA,UACxC,QAAQ;AAAA,UACR,UAAU;AAAA,YACR,kBAAkB,EAAE,iDAAiD,uBAAuB;AAAA,YAC5F,gBAAgB,EAAE,+CAA+C,0BAA0B;AAAA,YAC3F,YAAY,EAAE,2CAA2C,yBAAyB;AAAA,UACpF;AAAA,QACF,CAAC;AACD,YAAI;AACF,iBAAO,cAAc,IAAI,MAAM,oBAAoB,CAAC;AAAA,QACtD,QAAQ;AAAA,QAAC;AACT,cAAM,iBAAiB,EAAE,uCAAuC,gBAAgB;AAChF,sBAAc,QAAQ,0BAA0B,mBAAmB,QAAQ,CAAC,IAAI,gBAAgB,SAAS;AAAA,MAC3G;AAAA;AAAA,EACF,GACF,GACF;AAEJ;AASA,eAAe,2BAA2B,cAAmD;AAC3F,QAAM,OAAO,MAAM;AAAA,IACjB;AAAA,IACA;AAAA,IACA,EAAE,cAAc,gBAAgB,2BAA2B,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,EACrF;AACA,SAAO,MAAM,QAAQ,MAAM,KAAK,IAAI,KAAK,QAAS,CAAC;AACrD;AAIA,eAAe,2BAA2B,SAAkC;AAC1E,QAAM,WAAW,qBAAqB,OAAO;AAC/C;AAEA,eAAsB,mBAAmB,SASrB;AAClB,QAAM;AAAA,IACJ;AAAA,IACA,gBAAgB;AAAA,IAChB,eAAe;AAAA,IACf;AAAA,EACF,IAAI;AACJ,QAAM,cAAc,OAAO,OAAO,aAAa,WAAW,OAAO,SAAS,KAAK,IAAI;AACnF,MAAI,CAAC,aAAa;AAChB,UAAM,UAAU,UAAU,oBAAoB;AAC9C,UAAM,oBAAoB,SAAS,EAAE,UAAU,QAAQ,CAAC;AAAA,EAC1D;AAEA,QAAM,WAAW,MAAM,cAAc,UAAU,UAAU;AACzD,QAAM,SAAS,SAAS;AAAA,IACtB,CAAC,UAAU,OAAO,aAAa,gBAAgB,OAAO,WAAW,YAAY,OAAO,WAAW;AAAA,EACjG;AACA,MAAI,QAAQ;AACV,UAAM,UAAU,UAAU,kBAAkB;AAC5C,UAAM,oBAAoB,SAAS,EAAE,UAAU,QAAQ,CAAC;AAAA,EAC1D;AAEA,QAAM,UAAmC;AAAA,IACvC,GAAG;AAAA,IACH,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,eACE,OAAQ,OAAmC,kBAAkB,YAC5D,OAAmC,gBAC/B,OAAmC,gBACpC;AAAA,EACR;AAEA,QAAM,aAAa,OAAO;AAC1B,SAAO;AACT;",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { useRouter } from 'next/navigation'\nimport { pushWithFlash } from '@open-mercato/ui/backend/utils/flash'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\n\nconst schema = upsertCustomEntitySchema\n\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\n\nexport default function CreateEntityPage() {\n const t = useT()\n const router = useRouter()\n // Pre-fill the restriction toggle from the tenant default-restricted policy so\n // the form reflects the workspace posture; null while loading.\n const [defaultRestricted, setDefaultRestricted] = React.useState<boolean | null>(null)\n React.useEffect(() => {\n let active = true\n ;(async () => {\n try {\n const data = await readApiResultOrThrow<{ newEntitiesRestrictedByDefault?: boolean }>(\n '/api/entities/entity-settings',\n undefined,\n {\n errorMessage: t('entities.userEntities.errors.settingsLoadFailed', 'Could not load default restriction policy; using default'),\n fallback: { newEntitiesRestrictedByDefault: false },\n },\n )\n if (active) setDefaultRestricted(data?.newEntitiesRestrictedByDefault === true)\n } catch {\n flash(t('entities.userEntities.errors.settingsLoadFailed', 'Could not load default restriction policy; using default'), 'warning')\n if (active) setDefaultRestricted(false)\n }\n })()\n return () => { active = false }\n }, [t])\n const fields = React.useMemo<CrudField[]>(() => ([\n {\n id: 'entityId',\n label: t('entities.userEntities.form.entityId.label', 'Entity ID'),\n type: 'text',\n required: true,\n placeholder: t('entities.userEntities.form.entityId.placeholder', 'module_name:entity_id'),\n },\n { id: 'label', label: t('entities.userEntities.form.label.label', 'Label'), type: 'text', required: true },\n { id: 'description', label: t('entities.userEntities.form.description.label', 'Description'), type: 'textarea' },\n {\n id: 'defaultEditor',\n label: t('entities.userEntities.form.defaultEditor.label', 'Default Editor (multiline)'),\n type: 'select',\n options: [\n { value: '', label: t('entities.userEntities.form.defaultEditor.options.default', 'Default (Markdown)') },\n { value: 'markdown', label: t('entities.userEntities.form.defaultEditor.options.markdown', 'Markdown (UIW)') },\n { value: 'simpleMarkdown', label: t('entities.userEntities.form.defaultEditor.options.simpleMarkdown', 'Simple Markdown') },\n { value: 'htmlRichText', label: t('entities.userEntities.form.defaultEditor.options.htmlRichText', 'HTML Rich Text') },\n { value: 'plain', label: t('entities.userEntities.form.defaultEditor.options.plain', 'Plain textarea') },\n ],\n } as unknown as CrudField,\n { id: 'showInSidebar', label: t('entities.userEntities.form.showInSidebar.label', 'Show in sidebar'), type: 'checkbox' } as CrudField,\n {\n id: 'accessRestricted',\n label: t('entities.userEntities.form.accessRestricted.label', 'Restrict record access'),\n type: 'checkbox',\n description: t('entities.userEntities.form.accessRestricted.help', 'Require an explicit per-entity permission to view or manage this entity\u2019s records. Leave off to allow anyone with the general records permission.'),\n } as CrudField,\n ]), [t])\n\n if (defaultRestricted === null) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('entities.userEntities.form.loading', 'Loading\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={t('entities.userEntities.form.title', 'Create Entity')}\n backHref=\"/backend/entities/user\"\n schema={schema}\n fields={fields}\n initialValues={{ entityId: 'user:your_entity', label: '', showInSidebar: false, accessRestricted: defaultRestricted }}\n submitLabel={t('entities.userEntities.form.submit', 'Create')}\n cancelHref=\"/backend/entities/user\"\n onSubmit={async (vals) => {\n const entityId = await submitCreateEntity({\n values: vals as Record<string, unknown>,\n messages: {\n entityIdRequired: t('entities.userEntities.errors.entityIdRequired', 'Entity ID is required'),\n entityIdExists: t('entities.userEntities.errors.entityIdExists', 'Entity ID already exists'),\n loadFailed: t('entities.userEntities.errors.loadFailed', 'Failed to load entities'),\n },\n })\n try {\n window.dispatchEvent(new Event('om:refresh-sidebar'))\n } catch {}\n const successMessage = t('entities.userEntities.flash.created', 'Entity created')\n pushWithFlash(router, `/backend/entities/user/${encodeURIComponent(entityId)}`, successMessage, 'success')\n }}\n />\n </PageBody>\n </Page>\n )\n}\n\ntype EntityListEntry = {\n entityId?: string\n source?: string\n}\n\ntype FetchCustomEntities = (errorMessage?: string) => Promise<EntityListEntry[]>\n\nasync function defaultFetchCustomEntities(errorMessage?: string): Promise<EntityListEntry[]> {\n const data = await readApiResultOrThrow<{ items?: EntityListEntry[] }>(\n '/api/entities/entities',\n undefined,\n { errorMessage: errorMessage ?? 'Failed to load entities', fallback: { items: [] } },\n )\n return Array.isArray(data?.items) ? data.items! : []\n}\n\ntype CreateEntityRequest = (payload: Record<string, unknown>) => Promise<void>\n\nasync function defaultCreateEntityRequest(payload: Record<string, unknown>) {\n await createCrud('entities/entities', payload)\n}\n\nexport async function submitCreateEntity(options: {\n values: Record<string, unknown>\n fetchEntities?: FetchCustomEntities\n createEntity?: CreateEntityRequest\n messages?: {\n entityIdRequired?: string\n entityIdExists?: string\n loadFailed?: string\n }\n}): Promise<string> {\n const {\n values,\n fetchEntities = defaultFetchCustomEntities,\n createEntity = defaultCreateEntityRequest,\n messages,\n } = options\n const rawEntityId = typeof values.entityId === 'string' ? values.entityId.trim() : ''\n if (!rawEntityId) {\n const message = messages?.entityIdRequired ?? 'Entity ID is required'\n throw createCrudFormError(message, { entityId: message })\n }\n\n const existing = await fetchEntities(messages?.loadFailed)\n const exists = existing.some(\n (entry) => entry?.entityId === rawEntityId && (entry?.source === 'custom' || entry?.source === undefined),\n )\n if (exists) {\n const message = messages?.entityIdExists ?? 'Entity ID already exists'\n throw createCrudFormError(message, { entityId: message })\n }\n\n const payload: Record<string, unknown> = {\n ...values,\n entityId: rawEntityId,\n labelField: 'name',\n defaultEditor:\n typeof (values as Record<string, unknown>).defaultEditor === 'string' &&\n (values as Record<string, unknown>).defaultEditor\n ? (values as Record<string, unknown>).defaultEditor\n : undefined,\n }\n\n await createEntity(payload)\n return rawEntityId\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,136 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Button } from "@open-mercato/ui/primitives/button";
5
+ import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
6
+ import { Label } from "@open-mercato/ui/primitives/label";
7
+ import { Spinner } from "@open-mercato/ui/primitives/spinner";
8
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
9
+ import { apiCallOrThrow, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
10
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
11
+ import { ErrorMessage } from "@open-mercato/ui/backend/detail";
12
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
13
+ import { buildOptimisticLockHeader, extractOptimisticLockConflict } from "@open-mercato/ui/backend/utils/optimisticLock";
14
+ import { withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
15
+ function EntitySettingsManager() {
16
+ const t = useT();
17
+ const [loading, setLoading] = React.useState(true);
18
+ const [loadError, setLoadError] = React.useState(false);
19
+ const [saving, setSaving] = React.useState(false);
20
+ const [restricted, setRestricted] = React.useState(false);
21
+ const [updatedAt, setUpdatedAt] = React.useState(null);
22
+ const { runMutation } = useGuardedMutation({
23
+ contextId: "custom-entities-settings"
24
+ });
25
+ const loadSettings = React.useCallback(async () => {
26
+ setLoading(true);
27
+ setLoadError(false);
28
+ try {
29
+ const data = await readApiResultOrThrow(
30
+ "/api/entities/entity-settings",
31
+ void 0,
32
+ {
33
+ errorMessage: t("entities.settings.errors.loadFailed", "Failed to load settings")
34
+ }
35
+ );
36
+ setRestricted(data.newEntitiesRestrictedByDefault === true);
37
+ setUpdatedAt(data.updatedAt ?? null);
38
+ } catch {
39
+ setLoadError(true);
40
+ } finally {
41
+ setLoading(false);
42
+ }
43
+ }, [t]);
44
+ React.useEffect(() => {
45
+ void loadSettings();
46
+ }, [loadSettings]);
47
+ const handleSubmit = async (e) => {
48
+ e.preventDefault();
49
+ setSaving(true);
50
+ try {
51
+ const headers = buildOptimisticLockHeader(updatedAt);
52
+ await runMutation({
53
+ operation: () => withScopedApiRequestHeaders(headers, async () => {
54
+ const res = await apiCallOrThrow(
55
+ "/api/entities/entity-settings",
56
+ {
57
+ method: "PUT",
58
+ headers: { "content-type": "application/json" },
59
+ body: JSON.stringify({
60
+ newEntitiesRestrictedByDefault: restricted,
61
+ expectedUpdatedAt: updatedAt ?? void 0
62
+ })
63
+ },
64
+ {
65
+ errorMessage: t("entities.settings.errors.saveFailed", "Failed to save settings")
66
+ }
67
+ );
68
+ if (res.ok && res.result) {
69
+ setUpdatedAt(res.result.updatedAt);
70
+ flash(t("entities.settings.flash.saved", "Settings saved"), "success");
71
+ }
72
+ return res;
73
+ }),
74
+ context: {},
75
+ mutationPayload: {
76
+ newEntitiesRestrictedByDefault: restricted
77
+ }
78
+ });
79
+ } catch (err) {
80
+ const conflict = extractOptimisticLockConflict(err);
81
+ if (conflict) {
82
+ setLoading(true);
83
+ await loadSettings();
84
+ } else {
85
+ flash(t("entities.settings.errors.saveFailed", "Failed to save settings"), "error");
86
+ }
87
+ } finally {
88
+ setSaving(false);
89
+ }
90
+ };
91
+ if (loading) {
92
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
93
+ /* @__PURE__ */ jsx(Spinner, { className: "h-4 w-4" }),
94
+ /* @__PURE__ */ jsx("span", { children: t("entities.userEntities.form.loading", "Loading\u2026") })
95
+ ] });
96
+ }
97
+ if (loadError) {
98
+ return /* @__PURE__ */ jsx(
99
+ ErrorMessage,
100
+ {
101
+ label: t("entities.settings.errors.loadFailed", "Failed to load settings"),
102
+ action: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void loadSettings(), children: t("common.retry", "Retry") })
103
+ }
104
+ );
105
+ }
106
+ return /* @__PURE__ */ jsxs("form", { onSubmit: (e) => void handleSubmit(e), className: "space-y-6 max-w-2xl", children: [
107
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
108
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: t("entities.settings.title", "Custom Entities") }),
109
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("entities.settings.description", "Manage global custom entity configurations.") })
110
+ ] }),
111
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4 space-y-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
112
+ /* @__PURE__ */ jsx(
113
+ Checkbox,
114
+ {
115
+ id: "newEntitiesRestrictedByDefault",
116
+ checked: restricted,
117
+ onCheckedChange: (checked) => setRestricted(checked === true),
118
+ disabled: saving,
119
+ className: "mt-1"
120
+ }
121
+ ),
122
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
123
+ /* @__PURE__ */ jsx(Label, { htmlFor: "newEntitiesRestrictedByDefault", className: "font-medium cursor-pointer", children: t("entities.settings.newEntitiesRestrictedByDefault.label", "Restrict record access by default") }),
124
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("entities.settings.newEntitiesRestrictedByDefault.description", "Require explicit per-entity permissions for records of newly created custom entities by default.") })
125
+ ] })
126
+ ] }) }),
127
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-3 border-t pt-4", children: /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: saving, children: [
128
+ saving ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 h-4 w-4" }) : null,
129
+ t("common.save", "Save")
130
+ ] }) })
131
+ ] });
132
+ }
133
+ export {
134
+ EntitySettingsManager
135
+ };
136
+ //# sourceMappingURL=EntitySettingsManager.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/entities/components/EntitySettingsManager.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\n\nexport function EntitySettingsManager(): React.ReactElement {\n const t = useT()\n const [loading, setLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState(false)\n const [saving, setSaving] = React.useState(false)\n const [restricted, setRestricted] = React.useState(false)\n const [updatedAt, setUpdatedAt] = React.useState<string | null>(null)\n\n const { runMutation } = useGuardedMutation({\n contextId: 'custom-entities-settings',\n })\n\n const loadSettings = React.useCallback(async () => {\n setLoading(true)\n setLoadError(false)\n try {\n const data = await readApiResultOrThrow<{ newEntitiesRestrictedByDefault?: boolean; updatedAt?: string | null }>(\n '/api/entities/entity-settings',\n undefined,\n {\n errorMessage: t('entities.settings.errors.loadFailed', 'Failed to load settings'),\n }\n )\n setRestricted(data.newEntitiesRestrictedByDefault === true)\n setUpdatedAt(data.updatedAt ?? null)\n } catch {\n setLoadError(true)\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n void loadSettings()\n }, [loadSettings])\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setSaving(true)\n try {\n const headers = buildOptimisticLockHeader(updatedAt)\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(headers, async () => {\n const res = await apiCallOrThrow<{ ok: boolean; newEntitiesRestrictedByDefault: boolean; updatedAt: string | null }>(\n '/api/entities/entity-settings',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n newEntitiesRestrictedByDefault: restricted,\n expectedUpdatedAt: updatedAt ?? undefined,\n }),\n },\n {\n errorMessage: t('entities.settings.errors.saveFailed', 'Failed to save settings'),\n }\n )\n\n if (res.ok && res.result) {\n setUpdatedAt(res.result.updatedAt)\n flash(t('entities.settings.flash.saved', 'Settings saved'), 'success')\n }\n return res\n }),\n context: {},\n mutationPayload: {\n newEntitiesRestrictedByDefault: restricted,\n },\n })\n } catch (err) {\n const conflict = extractOptimisticLockConflict(err)\n if (conflict) {\n setLoading(true)\n await loadSettings()\n } else {\n flash(t('entities.settings.errors.saveFailed', 'Failed to save settings'), 'error')\n }\n } finally {\n setSaving(false)\n }\n }\n\n if (loading) {\n return (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n <span>{t('entities.userEntities.form.loading', 'Loading\u2026')}</span>\n </div>\n )\n }\n\n if (loadError) {\n return (\n <ErrorMessage\n label={t('entities.settings.errors.loadFailed', 'Failed to load settings')}\n action={(\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void loadSettings()}>\n {t('common.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n\n return (\n <form onSubmit={(e) => void handleSubmit(e)} className=\"space-y-6 max-w-2xl\">\n <div className=\"space-y-1\">\n <h3 className=\"text-base font-semibold\">\n {t('entities.settings.title', 'Custom Entities')}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('entities.settings.description', 'Manage global custom entity configurations.')}\n </p>\n </div>\n\n <div className=\"rounded-lg border bg-card p-4 space-y-4\">\n <div className=\"flex items-start gap-3\">\n <Checkbox\n id=\"newEntitiesRestrictedByDefault\"\n checked={restricted}\n onCheckedChange={(checked) => setRestricted(checked === true)}\n disabled={saving}\n className=\"mt-1\"\n />\n <div className=\"space-y-1\">\n <Label htmlFor=\"newEntitiesRestrictedByDefault\" className=\"font-medium cursor-pointer\">\n {t('entities.settings.newEntitiesRestrictedByDefault.label', 'Restrict record access by default')}\n </Label>\n <p className=\"text-sm text-muted-foreground\">\n {t('entities.settings.newEntitiesRestrictedByDefault.description', 'Require explicit per-entity permissions for records of newly created custom entities by default.')}\n </p>\n </div>\n </div>\n </div>\n\n <div className=\"flex items-center justify-end gap-3 border-t pt-4\">\n <Button type=\"submit\" disabled={saving}>\n {saving ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('common.save', 'Save')}\n </Button>\n </div>\n </form>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -10,11 +10,19 @@ import { parseInboundEmail } from "../../lib/emailParser.js";
10
10
  import { checkRateLimit } from "../../lib/rateLimiter.js";
11
11
  import { emitInboxOpsEvent } from "../../events.js";
12
12
  import { createLogger } from "@open-mercato/shared/lib/logger";
13
+ import {
14
+ readBoundedRequestBody,
15
+ resolveWebhookBodyLimitBytes,
16
+ WebhookBodyTooLargeError
17
+ } from "@open-mercato/shared/lib/webhooks";
13
18
  const logger = createLogger("inbox_ops").child({ component: "webhook" });
14
19
  const metadata = {
15
20
  POST: { requireAuth: false }
16
21
  };
17
- const MAX_PAYLOAD_SIZE = 2 * 1024 * 1024;
22
+ const MAX_PAYLOAD_SIZE = resolveWebhookBodyLimitBytes(
23
+ process.env.INBOX_OPS_WEBHOOK_MAX_BODY_BYTES ?? process.env.OM_WEBHOOK_MAX_BODY_BYTES,
24
+ 2 * 1024 * 1024
25
+ );
18
26
  const REPLAY_WINDOW_MS = 5 * 60 * 1e3;
19
27
  function isTimestampFresh(timestamp) {
20
28
  if (!timestamp) return false;
@@ -140,19 +148,15 @@ async function POST(req) {
140
148
  logger.error("Neither INBOX_OPS_WEBHOOK_SECRET nor RESEND_WEBHOOK_SIGNING_SECRET is configured");
141
149
  return NextResponse.json({ error: "Service unavailable" }, { status: 503 });
142
150
  }
143
- const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
144
- if (contentLength > MAX_PAYLOAD_SIZE) {
145
- return NextResponse.json({ error: "Payload too large" }, { status: 413 });
146
- }
147
151
  let rawBody;
148
152
  try {
149
- rawBody = await req.text();
150
- } catch {
153
+ rawBody = await readBoundedRequestBody(req, { maxBytes: MAX_PAYLOAD_SIZE });
154
+ } catch (error) {
155
+ if (error instanceof WebhookBodyTooLargeError) {
156
+ return NextResponse.json({ error: "Payload too large" }, { status: 413 });
157
+ }
151
158
  return NextResponse.json({ error: "Bad request" }, { status: 400 });
152
159
  }
153
- if (rawBody.length > MAX_PAYLOAD_SIZE) {
154
- return NextResponse.json({ error: "Payload too large" }, { status: 413 });
155
- }
156
160
  const verified = await verifyAndParse(req, rawBody);
157
161
  if (!verified.ok) {
158
162
  return verified.response;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/inbox_ops/api/webhook/inbound.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createHmac, timingSafeEqual } from 'node:crypto'\nimport { z } from 'zod'\nimport { Webhook } from 'svix'\nimport { Resend } from 'resend'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxSettings, InboxEmail } from '../../data/entities'\nimport { parseInboundEmail } from '../../lib/emailParser'\nimport { checkRateLimit } from '../../lib/rateLimiter'\nimport { emitInboxOpsEvent } from '../../events'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('inbox_ops').child({ component: 'webhook' })\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nconst MAX_PAYLOAD_SIZE = 2 * 1024 * 1024 // 2MB\nconst REPLAY_WINDOW_MS = 5 * 60 * 1000 // 5 minutes\n\nfunction isTimestampFresh(timestamp: string): boolean {\n if (!timestamp) return false\n const timestampMs = parseInt(timestamp, 10) * 1000\n if (isNaN(timestampMs)) return false\n return Math.abs(Date.now() - timestampMs) <= REPLAY_WINDOW_MS\n}\n\nfunction verifyHmacSignature(\n payload: string,\n signature: string,\n timestamp: string,\n secret: string,\n): boolean {\n if (!signature || !timestamp || !secret) return false\n\n if (!isTimestampFresh(timestamp)) return false\n\n const expected = createHmac('sha256', secret)\n .update(`${timestamp}.${payload}`)\n .digest('hex')\n const received = signature.startsWith('sha256=')\n ? signature.slice('sha256='.length)\n : signature\n\n try {\n if (received.length !== expected.length) return false\n return timingSafeEqual(\n Buffer.from(received, 'hex'),\n Buffer.from(expected, 'hex'),\n )\n } catch {\n return false\n }\n}\n\n// True when the signature verifies against ANY of the candidate secrets. Used\n// to bind the inbound signature to the resolved tenant: once the target inbox\n// declares its own per-tenant secret, the global INBOX_OPS_WEBHOOK_SECRET is no\n// longer accepted for that inbox, so a holder of the global key alone cannot\n// forge mail into a tenant that opted into a dedicated secret.\nfunction verifyHmacAgainstAny(\n payload: string,\n signature: string,\n timestamp: string,\n secrets: string[],\n): boolean {\n for (const secret of secrets) {\n if (secret && verifyHmacSignature(payload, signature, timestamp, secret)) return true\n }\n return false\n}\n\n// Stable, non-reversible identifier for the pre-DB global rate-limit bucket.\n// Keying on the configured signing secret (rather than the attacker-set `to` or\n// the per-request signature) means the bucket is request-invariant for a given\n// secret, so rotating recipients or mutating the body cannot dilute the limit.\nfunction secretFingerprint(secret: string): string {\n return createHmac('sha256', 'inbox_ops:rate_limit:salt').update(secret).digest('hex').slice(0, 32)\n}\n\nfunction verifySvixSignature(\n rawBody: string,\n headers: Record<string, string>,\n secret: string,\n): boolean {\n try {\n const wh = new Webhook(secret)\n wh.verify(rawBody, {\n 'svix-id': headers['svix-id'],\n 'svix-timestamp': headers['svix-timestamp'],\n 'svix-signature': headers['svix-signature'],\n })\n return true\n } catch {\n return false\n }\n}\n\nasync function fetchResendEmail(emailId: string): Promise<{\n from?: string\n to?: string[]\n subject?: string\n text?: string\n html?: string\n messageId?: string\n replyTo?: string\n inReplyTo?: string\n references?: string[]\n}> {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is required to fetch Resend inbound emails')\n\n const resend = new Resend(apiKey)\n const { data, error } = await resend.emails.receiving.get(emailId)\n if (error || !data) {\n throw new Error(`Failed to fetch Resend email ${emailId}: ${error?.message || 'unknown error'}`)\n }\n\n return {\n from: data.from,\n to: data.to,\n subject: data.subject,\n text: data.text ?? undefined,\n html: data.html ?? undefined,\n messageId: data.message_id,\n replyTo: data.reply_to?.[0] ?? undefined,\n inReplyTo: data.headers?.['in-reply-to'] ?? undefined,\n references: data.headers?.['references']?.split(/\\s+/).filter(Boolean),\n }\n}\n\ntype VerifiedPayload =\n | { kind: 'custom'; payload: Record<string, unknown>; signature: string; timestamp: string }\n | { kind: 'resend'; emailFields: Awaited<ReturnType<typeof fetchResendEmail>>; to: string[] }\n\nasync function verifyAndParse(req: Request, rawBody: string): Promise<\n | { ok: true; data: VerifiedPayload }\n | { ok: false; response: NextResponse }\n> {\n const customSig = req.headers.get('x-webhook-signature')\n const svixId = req.headers.get('svix-id')\n\n if (customSig) {\n const webhookSecret = process.env.INBOX_OPS_WEBHOOK_SECRET\n if (!webhookSecret) {\n logger.error('INBOX_OPS_WEBHOOK_SECRET not configured')\n return { ok: false, response: NextResponse.json({ error: 'Service unavailable' }, { status: 503 }) }\n }\n const timestamp = req.headers.get('x-webhook-timestamp') || ''\n // Cheap, secret-independent gate first: reject replayed/stale requests\n // before parsing. The HMAC itself is verified later, against the secret\n // bound to the resolved inbox (see the POST handler), so a holder of the\n // global secret cannot inject into a tenant that owns its own secret.\n if (!isTimestampFresh(timestamp)) {\n return { ok: false, response: NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) }\n }\n let payload: Record<string, unknown>\n try {\n payload = JSON.parse(rawBody)\n } catch {\n return { ok: false, response: NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n }\n return { ok: true, data: { kind: 'custom', payload, signature: customSig, timestamp } }\n }\n\n if (svixId) {\n const signingSecret = process.env.RESEND_WEBHOOK_SIGNING_SECRET\n if (!signingSecret) {\n logger.error('RESEND_WEBHOOK_SIGNING_SECRET not configured')\n return { ok: false, response: NextResponse.json({ error: 'Service unavailable' }, { status: 503 }) }\n }\n const headers: Record<string, string> = {\n 'svix-id': svixId,\n 'svix-timestamp': req.headers.get('svix-timestamp') || '',\n 'svix-signature': req.headers.get('svix-signature') || '',\n }\n if (!verifySvixSignature(rawBody, headers, signingSecret)) {\n return { ok: false, response: NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) }\n }\n let envelope: { type?: string; data?: { email_id?: string; from?: string; to?: string[] } }\n try {\n envelope = JSON.parse(rawBody)\n } catch {\n return { ok: false, response: NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n }\n if (envelope.type !== 'email.received') {\n return { ok: false, response: NextResponse.json({ ok: true }) }\n }\n const emailId = envelope.data?.email_id\n if (!emailId) {\n return { ok: false, response: NextResponse.json({ error: 'Missing email_id in Resend payload' }, { status: 400 }) }\n }\n const emailFields = await fetchResendEmail(emailId)\n const to = emailFields.to || envelope.data?.to || []\n return { ok: true, data: { kind: 'resend', emailFields, to } }\n }\n\n return { ok: false, response: NextResponse.json({ error: 'Missing signature headers' }, { status: 400 }) }\n}\n\nexport async function POST(req: Request) {\n const hasCustomSecret = Boolean(process.env.INBOX_OPS_WEBHOOK_SECRET)\n const hasResendSecret = Boolean(process.env.RESEND_WEBHOOK_SIGNING_SECRET)\n if (!hasCustomSecret && !hasResendSecret) {\n logger.error('Neither INBOX_OPS_WEBHOOK_SECRET nor RESEND_WEBHOOK_SIGNING_SECRET is configured')\n return NextResponse.json({ error: 'Service unavailable' }, { status: 503 })\n }\n\n const contentLength = parseInt(req.headers.get('content-length') || '0', 10)\n if (contentLength > MAX_PAYLOAD_SIZE) {\n return NextResponse.json({ error: 'Payload too large' }, { status: 413 })\n }\n\n let rawBody: string\n try {\n rawBody = await req.text()\n } catch {\n return NextResponse.json({ error: 'Bad request' }, { status: 400 })\n }\n\n if (rawBody.length > MAX_PAYLOAD_SIZE) {\n return NextResponse.json({ error: 'Payload too large' }, { status: 413 })\n }\n\n const verified = await verifyAndParse(req, rawBody)\n if (!verified.ok) {\n return verified.response\n }\n\n let toAddress: string | null\n let emailInput: Parameters<typeof parseInboundEmail>[0]\n\n if (verified.data.kind === 'custom') {\n const payload = verified.data.payload\n toAddress = extractToAddress(payload)\n emailInput = {\n from: payload.from as string | undefined,\n to: payload.to as string | string[] | undefined,\n subject: payload.subject as string | undefined,\n text: payload.text as string | undefined,\n html: payload.html as string | undefined,\n messageId: payload.messageId as string | undefined,\n replyTo: payload.replyTo as string | undefined,\n inReplyTo: payload.inReplyTo as string | undefined,\n references: payload.references as string | string[] | undefined,\n }\n } else {\n const { emailFields, to } = verified.data\n toAddress = to.length > 0 ? to[0].toLowerCase() : null\n emailInput = {\n from: emailFields.from,\n to: emailFields.to,\n subject: emailFields.subject,\n text: emailFields.text,\n html: emailFields.html,\n messageId: emailFields.messageId,\n replyTo: emailFields.replyTo,\n inReplyTo: emailFields.inReplyTo,\n references: emailFields.references,\n }\n }\n\n if (!toAddress) {\n return NextResponse.json({ error: 'Missing recipient address' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n\n let cache: CacheStrategy | null = null\n try {\n cache = container.resolve('cache') as CacheStrategy\n } catch {\n // Cache not available \u2014 the limiter now fails closed with a process-local\n // bucket instead of waving the request through.\n }\n\n // Pre-DB global bucket keyed on the configured signing secret (custom) or the\n // static Resend path, NOT on the attacker-controllable `to` and NOT on the\n // per-request signature (which mutates with timestamp/body, so it would mint a\n // fresh bucket every request and never throttle). The per-tenant secret lives\n // behind the encrypted-column lookup, so the only secret available here without\n // a DB hit is the global one \u2014 keying on it caps total inbound volume for the\n // custom path and runs BEFORE the lookup so a flood cannot amplify DB work.\n const globalBucketKey = verified.data.kind === 'custom'\n ? `webhook:secret:${secretFingerprint(process.env.INBOX_OPS_WEBHOOK_SECRET || '')}`\n : 'webhook:resend'\n const globalRateCheck = await checkRateLimit(cache, globalBucketKey)\n if (!globalRateCheck.allowed) {\n return rateLimitedResponse(globalRateCheck.retryAfterSeconds)\n }\n\n const em = (container.resolve('em') as EntityManager).fork()\n\n // No explicit scope: decryptEntitiesWithFallbackScope reads tenant/org off the\n // resolved row, which is exactly what binds webhook_secret to its own tenant.\n const settings = await findOneWithDecryption(\n em,\n InboxSettings,\n {\n inboxAddress: toAddress.toLowerCase(),\n isActive: true,\n deletedAt: null,\n },\n )\n\n // Final HMAC verification for the custom provider path: bind the signature to\n // the resolved tenant. When the inbox declares its own secret, ONLY that\n // secret is accepted; otherwise the global secret is accepted (back-compat).\n // For an unknown `to` we still require the global secret so the endpoint never\n // behaves as an unauthenticated probe oracle.\n if (verified.data.kind === 'custom') {\n const globalSecret = process.env.INBOX_OPS_WEBHOOK_SECRET || ''\n const inboxSecret = settings?.webhookSecret || ''\n const acceptableSecrets = inboxSecret ? [inboxSecret] : [globalSecret]\n const signatureValid = verifyHmacAgainstAny(\n rawBody,\n verified.data.signature,\n verified.data.timestamp,\n acceptableSecrets,\n )\n if (!signatureValid) {\n return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })\n }\n }\n\n if (!settings) {\n return NextResponse.json({ ok: true })\n }\n\n // Per-tenant bucket keyed on the RESOLVED tenant (not the raw `to`), so it\n // cannot be diluted by alias addresses that map to the same inbox.\n const tenantRateCheck = await checkRateLimit(\n cache,\n `webhook:tenant:${settings.tenantId}`,\n settings.tenantId,\n )\n if (!tenantRateCheck.allowed) {\n return rateLimitedResponse(tenantRateCheck.retryAfterSeconds)\n }\n\n const parsed = parseInboundEmail(emailInput)\n\n const isDuplicate = await checkDuplicate(em, settings, parsed.messageId, parsed.contentHash)\n if (isDuplicate) {\n try {\n await emitInboxOpsEvent('inbox_ops.email.deduplicated', {\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n toAddress,\n })\n } catch (eventError) {\n logger.error('Failed to emit deduplicated event', { err: eventError })\n }\n return NextResponse.json({ ok: true })\n }\n\n const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || '204800', 10)\n const cleanedText = parsed.cleanedText.slice(0, maxTextSize)\n\n const email = em.create(InboxEmail, {\n messageId: parsed.messageId,\n contentHash: parsed.contentHash,\n forwardedByAddress: parsed.from.email,\n forwardedByName: parsed.from.name || null,\n toAddress,\n subject: parsed.subject,\n replyTo: parsed.replyTo,\n inReplyTo: parsed.inReplyTo,\n emailReferences: parsed.references,\n rawText: parsed.rawText,\n rawHtml: parsed.rawHtml,\n cleanedText,\n threadMessages: parsed.threadMessages,\n detectedLanguage: parsed.detectedLanguage,\n receivedAt: new Date(),\n status: 'received' as const,\n isActive: true,\n organizationId: settings.organizationId,\n tenantId: settings.tenantId,\n })\n\n em.persist(email)\n await em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.received', {\n emailId: email.id,\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n forwardedByAddress: parsed.from.email,\n subject: parsed.subject,\n })\n } catch (eventError) {\n logger.error('Failed to emit email.received event', { err: eventError })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nfunction rateLimitedResponse(retryAfterSeconds?: number): NextResponse {\n return NextResponse.json(\n { error: 'Rate limit exceeded' },\n {\n status: 429,\n headers: retryAfterSeconds ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n },\n )\n}\n\nfunction extractToAddress(payload: Record<string, unknown>): string | null {\n const to = payload.to\n if (typeof to === 'string') {\n const match = to.match(/<([^>]+)>/)\n return match ? match[1].toLowerCase() : to.trim().toLowerCase()\n }\n if (Array.isArray(to) && to.length > 0) {\n const first = to[0]\n if (typeof first === 'string') {\n const match = first.match(/<([^>]+)>/)\n return match ? match[1].toLowerCase() : first.trim().toLowerCase()\n }\n }\n return null\n}\n\nasync function checkDuplicate(\n em: EntityManager,\n settings: InboxSettings,\n messageId: string | null | undefined,\n contentHash: string | null | undefined,\n): Promise<boolean> {\n const encScope = {\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n }\n const whereBase = {\n organizationId: settings.organizationId,\n tenantId: settings.tenantId,\n deletedAt: null,\n }\n\n if (messageId) {\n const byMessageId = await findOneWithDecryption(em, InboxEmail, { ...whereBase, messageId }, undefined, encScope)\n if (byMessageId) return true\n }\n\n if (contentHash) {\n const byHash = await findOneWithDecryption(em, InboxEmail, { ...whereBase, contentHash }, undefined, encScope)\n if (byHash) return true\n }\n\n return false\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Inbound email webhook',\n methods: {\n POST: {\n summary: 'Receive forwarded email from provider webhook',\n description: 'Public endpoint \u2014 validated by provider HMAC signature or Resend/Svix signature. Rate limited per tenant.',\n requestBody: {\n contentType: 'application/json',\n schema: z.object({\n from: z.string().optional(),\n to: z.union([z.string(), z.array(z.string())]).optional(),\n subject: z.string().optional(),\n text: z.string().optional(),\n html: z.string().optional(),\n messageId: z.string().optional(),\n replyTo: z.string().optional(),\n inReplyTo: z.string().optional(),\n references: z.union([z.string(), z.array(z.string())]).optional(),\n }),\n },\n responses: [\n { status: 200, description: 'Email received and queued for processing' },\n { status: 400, description: 'Invalid payload or signature' },\n { status: 413, description: 'Payload too large' },\n { status: 429, description: 'Rate limit exceeded' },\n { status: 503, description: 'Webhook secret not configured' },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createHmac, timingSafeEqual } from 'node:crypto'\nimport { z } from 'zod'\nimport { Webhook } from 'svix'\nimport { Resend } from 'resend'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxSettings, InboxEmail } from '../../data/entities'\nimport { parseInboundEmail } from '../../lib/emailParser'\nimport { checkRateLimit } from '../../lib/rateLimiter'\nimport { emitInboxOpsEvent } from '../../events'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n readBoundedRequestBody,\n resolveWebhookBodyLimitBytes,\n WebhookBodyTooLargeError,\n} from '@open-mercato/shared/lib/webhooks'\n\nconst logger = createLogger('inbox_ops').child({ component: 'webhook' })\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nconst MAX_PAYLOAD_SIZE = resolveWebhookBodyLimitBytes(\n process.env.INBOX_OPS_WEBHOOK_MAX_BODY_BYTES ?? process.env.OM_WEBHOOK_MAX_BODY_BYTES,\n 2 * 1024 * 1024,\n)\nconst REPLAY_WINDOW_MS = 5 * 60 * 1000 // 5 minutes\n\nfunction isTimestampFresh(timestamp: string): boolean {\n if (!timestamp) return false\n const timestampMs = parseInt(timestamp, 10) * 1000\n if (isNaN(timestampMs)) return false\n return Math.abs(Date.now() - timestampMs) <= REPLAY_WINDOW_MS\n}\n\nfunction verifyHmacSignature(\n payload: string,\n signature: string,\n timestamp: string,\n secret: string,\n): boolean {\n if (!signature || !timestamp || !secret) return false\n\n if (!isTimestampFresh(timestamp)) return false\n\n const expected = createHmac('sha256', secret)\n .update(`${timestamp}.${payload}`)\n .digest('hex')\n const received = signature.startsWith('sha256=')\n ? signature.slice('sha256='.length)\n : signature\n\n try {\n if (received.length !== expected.length) return false\n return timingSafeEqual(\n Buffer.from(received, 'hex'),\n Buffer.from(expected, 'hex'),\n )\n } catch {\n return false\n }\n}\n\n// True when the signature verifies against ANY of the candidate secrets. Used\n// to bind the inbound signature to the resolved tenant: once the target inbox\n// declares its own per-tenant secret, the global INBOX_OPS_WEBHOOK_SECRET is no\n// longer accepted for that inbox, so a holder of the global key alone cannot\n// forge mail into a tenant that opted into a dedicated secret.\nfunction verifyHmacAgainstAny(\n payload: string,\n signature: string,\n timestamp: string,\n secrets: string[],\n): boolean {\n for (const secret of secrets) {\n if (secret && verifyHmacSignature(payload, signature, timestamp, secret)) return true\n }\n return false\n}\n\n// Stable, non-reversible identifier for the pre-DB global rate-limit bucket.\n// Keying on the configured signing secret (rather than the attacker-set `to` or\n// the per-request signature) means the bucket is request-invariant for a given\n// secret, so rotating recipients or mutating the body cannot dilute the limit.\nfunction secretFingerprint(secret: string): string {\n return createHmac('sha256', 'inbox_ops:rate_limit:salt').update(secret).digest('hex').slice(0, 32)\n}\n\nfunction verifySvixSignature(\n rawBody: string,\n headers: Record<string, string>,\n secret: string,\n): boolean {\n try {\n const wh = new Webhook(secret)\n wh.verify(rawBody, {\n 'svix-id': headers['svix-id'],\n 'svix-timestamp': headers['svix-timestamp'],\n 'svix-signature': headers['svix-signature'],\n })\n return true\n } catch {\n return false\n }\n}\n\nasync function fetchResendEmail(emailId: string): Promise<{\n from?: string\n to?: string[]\n subject?: string\n text?: string\n html?: string\n messageId?: string\n replyTo?: string\n inReplyTo?: string\n references?: string[]\n}> {\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is required to fetch Resend inbound emails')\n\n const resend = new Resend(apiKey)\n const { data, error } = await resend.emails.receiving.get(emailId)\n if (error || !data) {\n throw new Error(`Failed to fetch Resend email ${emailId}: ${error?.message || 'unknown error'}`)\n }\n\n return {\n from: data.from,\n to: data.to,\n subject: data.subject,\n text: data.text ?? undefined,\n html: data.html ?? undefined,\n messageId: data.message_id,\n replyTo: data.reply_to?.[0] ?? undefined,\n inReplyTo: data.headers?.['in-reply-to'] ?? undefined,\n references: data.headers?.['references']?.split(/\\s+/).filter(Boolean),\n }\n}\n\ntype VerifiedPayload =\n | { kind: 'custom'; payload: Record<string, unknown>; signature: string; timestamp: string }\n | { kind: 'resend'; emailFields: Awaited<ReturnType<typeof fetchResendEmail>>; to: string[] }\n\nasync function verifyAndParse(req: Request, rawBody: string): Promise<\n | { ok: true; data: VerifiedPayload }\n | { ok: false; response: NextResponse }\n> {\n const customSig = req.headers.get('x-webhook-signature')\n const svixId = req.headers.get('svix-id')\n\n if (customSig) {\n const webhookSecret = process.env.INBOX_OPS_WEBHOOK_SECRET\n if (!webhookSecret) {\n logger.error('INBOX_OPS_WEBHOOK_SECRET not configured')\n return { ok: false, response: NextResponse.json({ error: 'Service unavailable' }, { status: 503 }) }\n }\n const timestamp = req.headers.get('x-webhook-timestamp') || ''\n // Cheap, secret-independent gate first: reject replayed/stale requests\n // before parsing. The HMAC itself is verified later, against the secret\n // bound to the resolved inbox (see the POST handler), so a holder of the\n // global secret cannot inject into a tenant that owns its own secret.\n if (!isTimestampFresh(timestamp)) {\n return { ok: false, response: NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) }\n }\n let payload: Record<string, unknown>\n try {\n payload = JSON.parse(rawBody)\n } catch {\n return { ok: false, response: NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n }\n return { ok: true, data: { kind: 'custom', payload, signature: customSig, timestamp } }\n }\n\n if (svixId) {\n const signingSecret = process.env.RESEND_WEBHOOK_SIGNING_SECRET\n if (!signingSecret) {\n logger.error('RESEND_WEBHOOK_SIGNING_SECRET not configured')\n return { ok: false, response: NextResponse.json({ error: 'Service unavailable' }, { status: 503 }) }\n }\n const headers: Record<string, string> = {\n 'svix-id': svixId,\n 'svix-timestamp': req.headers.get('svix-timestamp') || '',\n 'svix-signature': req.headers.get('svix-signature') || '',\n }\n if (!verifySvixSignature(rawBody, headers, signingSecret)) {\n return { ok: false, response: NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) }\n }\n let envelope: { type?: string; data?: { email_id?: string; from?: string; to?: string[] } }\n try {\n envelope = JSON.parse(rawBody)\n } catch {\n return { ok: false, response: NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n }\n if (envelope.type !== 'email.received') {\n return { ok: false, response: NextResponse.json({ ok: true }) }\n }\n const emailId = envelope.data?.email_id\n if (!emailId) {\n return { ok: false, response: NextResponse.json({ error: 'Missing email_id in Resend payload' }, { status: 400 }) }\n }\n const emailFields = await fetchResendEmail(emailId)\n const to = emailFields.to || envelope.data?.to || []\n return { ok: true, data: { kind: 'resend', emailFields, to } }\n }\n\n return { ok: false, response: NextResponse.json({ error: 'Missing signature headers' }, { status: 400 }) }\n}\n\nexport async function POST(req: Request) {\n const hasCustomSecret = Boolean(process.env.INBOX_OPS_WEBHOOK_SECRET)\n const hasResendSecret = Boolean(process.env.RESEND_WEBHOOK_SIGNING_SECRET)\n if (!hasCustomSecret && !hasResendSecret) {\n logger.error('Neither INBOX_OPS_WEBHOOK_SECRET nor RESEND_WEBHOOK_SIGNING_SECRET is configured')\n return NextResponse.json({ error: 'Service unavailable' }, { status: 503 })\n }\n\n let rawBody: string\n try {\n rawBody = await readBoundedRequestBody(req, { maxBytes: MAX_PAYLOAD_SIZE })\n } catch (error) {\n if (error instanceof WebhookBodyTooLargeError) {\n return NextResponse.json({ error: 'Payload too large' }, { status: 413 })\n }\n return NextResponse.json({ error: 'Bad request' }, { status: 400 })\n }\n\n const verified = await verifyAndParse(req, rawBody)\n if (!verified.ok) {\n return verified.response\n }\n\n let toAddress: string | null\n let emailInput: Parameters<typeof parseInboundEmail>[0]\n\n if (verified.data.kind === 'custom') {\n const payload = verified.data.payload\n toAddress = extractToAddress(payload)\n emailInput = {\n from: payload.from as string | undefined,\n to: payload.to as string | string[] | undefined,\n subject: payload.subject as string | undefined,\n text: payload.text as string | undefined,\n html: payload.html as string | undefined,\n messageId: payload.messageId as string | undefined,\n replyTo: payload.replyTo as string | undefined,\n inReplyTo: payload.inReplyTo as string | undefined,\n references: payload.references as string | string[] | undefined,\n }\n } else {\n const { emailFields, to } = verified.data\n toAddress = to.length > 0 ? to[0].toLowerCase() : null\n emailInput = {\n from: emailFields.from,\n to: emailFields.to,\n subject: emailFields.subject,\n text: emailFields.text,\n html: emailFields.html,\n messageId: emailFields.messageId,\n replyTo: emailFields.replyTo,\n inReplyTo: emailFields.inReplyTo,\n references: emailFields.references,\n }\n }\n\n if (!toAddress) {\n return NextResponse.json({ error: 'Missing recipient address' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n\n let cache: CacheStrategy | null = null\n try {\n cache = container.resolve('cache') as CacheStrategy\n } catch {\n // Cache not available \u2014 the limiter now fails closed with a process-local\n // bucket instead of waving the request through.\n }\n\n // Pre-DB global bucket keyed on the configured signing secret (custom) or the\n // static Resend path, NOT on the attacker-controllable `to` and NOT on the\n // per-request signature (which mutates with timestamp/body, so it would mint a\n // fresh bucket every request and never throttle). The per-tenant secret lives\n // behind the encrypted-column lookup, so the only secret available here without\n // a DB hit is the global one \u2014 keying on it caps total inbound volume for the\n // custom path and runs BEFORE the lookup so a flood cannot amplify DB work.\n const globalBucketKey = verified.data.kind === 'custom'\n ? `webhook:secret:${secretFingerprint(process.env.INBOX_OPS_WEBHOOK_SECRET || '')}`\n : 'webhook:resend'\n const globalRateCheck = await checkRateLimit(cache, globalBucketKey)\n if (!globalRateCheck.allowed) {\n return rateLimitedResponse(globalRateCheck.retryAfterSeconds)\n }\n\n const em = (container.resolve('em') as EntityManager).fork()\n\n // No explicit scope: decryptEntitiesWithFallbackScope reads tenant/org off the\n // resolved row, which is exactly what binds webhook_secret to its own tenant.\n const settings = await findOneWithDecryption(\n em,\n InboxSettings,\n {\n inboxAddress: toAddress.toLowerCase(),\n isActive: true,\n deletedAt: null,\n },\n )\n\n // Final HMAC verification for the custom provider path: bind the signature to\n // the resolved tenant. When the inbox declares its own secret, ONLY that\n // secret is accepted; otherwise the global secret is accepted (back-compat).\n // For an unknown `to` we still require the global secret so the endpoint never\n // behaves as an unauthenticated probe oracle.\n if (verified.data.kind === 'custom') {\n const globalSecret = process.env.INBOX_OPS_WEBHOOK_SECRET || ''\n const inboxSecret = settings?.webhookSecret || ''\n const acceptableSecrets = inboxSecret ? [inboxSecret] : [globalSecret]\n const signatureValid = verifyHmacAgainstAny(\n rawBody,\n verified.data.signature,\n verified.data.timestamp,\n acceptableSecrets,\n )\n if (!signatureValid) {\n return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })\n }\n }\n\n if (!settings) {\n return NextResponse.json({ ok: true })\n }\n\n // Per-tenant bucket keyed on the RESOLVED tenant (not the raw `to`), so it\n // cannot be diluted by alias addresses that map to the same inbox.\n const tenantRateCheck = await checkRateLimit(\n cache,\n `webhook:tenant:${settings.tenantId}`,\n settings.tenantId,\n )\n if (!tenantRateCheck.allowed) {\n return rateLimitedResponse(tenantRateCheck.retryAfterSeconds)\n }\n\n const parsed = parseInboundEmail(emailInput)\n\n const isDuplicate = await checkDuplicate(em, settings, parsed.messageId, parsed.contentHash)\n if (isDuplicate) {\n try {\n await emitInboxOpsEvent('inbox_ops.email.deduplicated', {\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n toAddress,\n })\n } catch (eventError) {\n logger.error('Failed to emit deduplicated event', { err: eventError })\n }\n return NextResponse.json({ ok: true })\n }\n\n const maxTextSize = parseInt(process.env.INBOX_OPS_MAX_TEXT_SIZE || '204800', 10)\n const cleanedText = parsed.cleanedText.slice(0, maxTextSize)\n\n const email = em.create(InboxEmail, {\n messageId: parsed.messageId,\n contentHash: parsed.contentHash,\n forwardedByAddress: parsed.from.email,\n forwardedByName: parsed.from.name || null,\n toAddress,\n subject: parsed.subject,\n replyTo: parsed.replyTo,\n inReplyTo: parsed.inReplyTo,\n emailReferences: parsed.references,\n rawText: parsed.rawText,\n rawHtml: parsed.rawHtml,\n cleanedText,\n threadMessages: parsed.threadMessages,\n detectedLanguage: parsed.detectedLanguage,\n receivedAt: new Date(),\n status: 'received' as const,\n isActive: true,\n organizationId: settings.organizationId,\n tenantId: settings.tenantId,\n })\n\n em.persist(email)\n await em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.email.received', {\n emailId: email.id,\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n forwardedByAddress: parsed.from.email,\n subject: parsed.subject,\n })\n } catch (eventError) {\n logger.error('Failed to emit email.received event', { err: eventError })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nfunction rateLimitedResponse(retryAfterSeconds?: number): NextResponse {\n return NextResponse.json(\n { error: 'Rate limit exceeded' },\n {\n status: 429,\n headers: retryAfterSeconds ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n },\n )\n}\n\nfunction extractToAddress(payload: Record<string, unknown>): string | null {\n const to = payload.to\n if (typeof to === 'string') {\n const match = to.match(/<([^>]+)>/)\n return match ? match[1].toLowerCase() : to.trim().toLowerCase()\n }\n if (Array.isArray(to) && to.length > 0) {\n const first = to[0]\n if (typeof first === 'string') {\n const match = first.match(/<([^>]+)>/)\n return match ? match[1].toLowerCase() : first.trim().toLowerCase()\n }\n }\n return null\n}\n\nasync function checkDuplicate(\n em: EntityManager,\n settings: InboxSettings,\n messageId: string | null | undefined,\n contentHash: string | null | undefined,\n): Promise<boolean> {\n const encScope = {\n tenantId: settings.tenantId,\n organizationId: settings.organizationId,\n }\n const whereBase = {\n organizationId: settings.organizationId,\n tenantId: settings.tenantId,\n deletedAt: null,\n }\n\n if (messageId) {\n const byMessageId = await findOneWithDecryption(em, InboxEmail, { ...whereBase, messageId }, undefined, encScope)\n if (byMessageId) return true\n }\n\n if (contentHash) {\n const byHash = await findOneWithDecryption(em, InboxEmail, { ...whereBase, contentHash }, undefined, encScope)\n if (byHash) return true\n }\n\n return false\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Inbound email webhook',\n methods: {\n POST: {\n summary: 'Receive forwarded email from provider webhook',\n description: 'Public endpoint \u2014 validated by provider HMAC signature or Resend/Svix signature. Rate limited per tenant.',\n requestBody: {\n contentType: 'application/json',\n schema: z.object({\n from: z.string().optional(),\n to: z.union([z.string(), z.array(z.string())]).optional(),\n subject: z.string().optional(),\n text: z.string().optional(),\n html: z.string().optional(),\n messageId: z.string().optional(),\n replyTo: z.string().optional(),\n inReplyTo: z.string().optional(),\n references: z.union([z.string(), z.array(z.string())]).optional(),\n }),\n },\n responses: [\n { status: 200, description: 'Email received and queued for processing' },\n { status: 400, description: 'Invalid payload or signature' },\n { status: 413, description: 'Payload too large' },\n { status: 429, description: 'Rate limit exceeded' },\n { status: 503, description: 'Webhook secret not configured' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }