@open-mercato/core 0.6.8-develop.6983.1.e4f794099e → 0.6.8-develop.6985.1.fb93574faa

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 (597) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/notification/index.js +6 -0
  3. package/dist/generated/entities/notification/index.js.map +2 -2
  4. package/dist/generated/entities/notification_preference/index.js +19 -0
  5. package/dist/generated/entities/notification_preference/index.js.map +7 -0
  6. package/dist/generated/entities/notification_type/index.js +21 -0
  7. package/dist/generated/entities/notification_type/index.js.map +7 -0
  8. package/dist/generated/entities/notification_type_override/index.js +17 -0
  9. package/dist/generated/entities/notification_type_override/index.js.map +7 -0
  10. package/dist/generated/entities/push_notification_delivery/index.js +41 -0
  11. package/dist/generated/entities/push_notification_delivery/index.js.map +7 -0
  12. package/dist/generated/entities/user_device/index.js +35 -0
  13. package/dist/generated/entities/user_device/index.js.map +7 -0
  14. package/dist/generated/entities.ids.generated.js +12 -1
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +72 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/api.js +2 -1
  19. package/dist/helpers/integration/api.js.map +2 -2
  20. package/dist/helpers/integration/appRoot.js +9 -0
  21. package/dist/helpers/integration/appRoot.js.map +7 -0
  22. package/dist/helpers/integration/dbFixtures.js +1 -4
  23. package/dist/helpers/integration/dbFixtures.js.map +2 -2
  24. package/dist/helpers/integration/pushFake.js +126 -0
  25. package/dist/helpers/integration/pushFake.js.map +7 -0
  26. package/dist/modules/auth/notifications.js +16 -0
  27. package/dist/modules/auth/notifications.js.map +2 -2
  28. package/dist/modules/business_rules/notifications.js +2 -0
  29. package/dist/modules/business_rules/notifications.js.map +2 -2
  30. package/dist/modules/catalog/notifications.js +2 -0
  31. package/dist/modules/catalog/notifications.js.map +2 -2
  32. package/dist/modules/communication_channels/acl.js +9 -0
  33. package/dist/modules/communication_channels/acl.js.map +2 -2
  34. package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js +133 -0
  35. package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js.map +7 -0
  36. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +6 -2
  37. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +2 -2
  38. package/dist/modules/communication_channels/api/get/channels/[id]/route.js +6 -2
  39. package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +2 -2
  40. package/dist/modules/communication_channels/api/get/channels/route.js +2 -1
  41. package/dist/modules/communication_channels/api/get/channels/route.js.map +2 -2
  42. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +10 -5
  43. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
  44. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +29 -1
  45. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  46. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +157 -0
  47. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +7 -0
  48. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +8 -1
  49. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
  50. package/dist/modules/communication_channels/commands/admin-delete-channel.js +141 -0
  51. package/dist/modules/communication_channels/commands/admin-delete-channel.js.map +7 -0
  52. package/dist/modules/communication_channels/commands/connect-credential-channel.js +25 -8
  53. package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
  54. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +2 -2
  55. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  56. package/dist/modules/communication_channels/commands/push-register.js +1 -1
  57. package/dist/modules/communication_channels/commands/push-register.js.map +2 -2
  58. package/dist/modules/communication_channels/commands/push-unregister.js +4 -3
  59. package/dist/modules/communication_channels/commands/push-unregister.js.map +2 -2
  60. package/dist/modules/communication_channels/data/entities.js +4 -0
  61. package/dist/modules/communication_channels/data/entities.js.map +2 -2
  62. package/dist/modules/communication_channels/data/validators.js +2 -1
  63. package/dist/modules/communication_channels/data/validators.js.map +2 -2
  64. package/dist/modules/communication_channels/lib/access-control.js +11 -1
  65. package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
  66. package/dist/modules/communication_channels/lib/connect-channel.js +20 -12
  67. package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
  68. package/dist/modules/communication_channels/lib/push-adapter.js +68 -0
  69. package/dist/modules/communication_channels/lib/push-adapter.js.map +7 -0
  70. package/dist/modules/communication_channels/lib/push-capabilities.js +31 -0
  71. package/dist/modules/communication_channels/lib/push-capabilities.js.map +7 -0
  72. package/dist/modules/communication_channels/lib/push-connect-error.js +20 -0
  73. package/dist/modules/communication_channels/lib/push-connect-error.js.map +7 -0
  74. package/dist/modules/communication_channels/lib/push-credential-errors.js +41 -0
  75. package/dist/modules/communication_channels/lib/push-credential-errors.js.map +7 -0
  76. package/dist/modules/communication_channels/lib/push-envelope.js +46 -0
  77. package/dist/modules/communication_channels/lib/push-envelope.js.map +7 -0
  78. package/dist/modules/communication_channels/lib/refcounted-client-cache.js +64 -0
  79. package/dist/modules/communication_channels/lib/refcounted-client-cache.js.map +7 -0
  80. package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js +13 -0
  81. package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js.map +7 -0
  82. package/dist/modules/communication_channels/notifications.js +4 -0
  83. package/dist/modules/communication_channels/notifications.js.map +2 -2
  84. package/dist/modules/communication_channels/setup.js +2 -0
  85. package/dist/modules/communication_channels/setup.js.map +2 -2
  86. package/dist/modules/configs/lib/upgrade-actions.js +21 -0
  87. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  88. package/dist/modules/customer_accounts/notifications.js +12 -0
  89. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  90. package/dist/modules/customers/notifications.js +4 -0
  91. package/dist/modules/customers/notifications.js.map +2 -2
  92. package/dist/modules/devices/acl.js +11 -0
  93. package/dist/modules/devices/acl.js.map +7 -0
  94. package/dist/modules/devices/api/[id]/route.js +167 -0
  95. package/dist/modules/devices/api/[id]/route.js.map +7 -0
  96. package/dist/modules/devices/api/admin/devices/[id]/route.js +219 -0
  97. package/dist/modules/devices/api/admin/devices/[id]/route.js.map +7 -0
  98. package/dist/modules/devices/api/admin/devices/route.js +140 -0
  99. package/dist/modules/devices/api/admin/devices/route.js.map +7 -0
  100. package/dist/modules/devices/api/auth.js +9 -0
  101. package/dist/modules/devices/api/auth.js.map +7 -0
  102. package/dist/modules/devices/api/deviceList.js +53 -0
  103. package/dist/modules/devices/api/deviceList.js.map +7 -0
  104. package/dist/modules/devices/api/deviceOps.js +118 -0
  105. package/dist/modules/devices/api/deviceOps.js.map +7 -0
  106. package/dist/modules/devices/api/openapi.js +19 -0
  107. package/dist/modules/devices/api/openapi.js.map +7 -0
  108. package/dist/modules/devices/api/route.js +143 -0
  109. package/dist/modules/devices/api/route.js.map +7 -0
  110. package/dist/modules/devices/backend/devices/[id]/page.js +135 -0
  111. package/dist/modules/devices/backend/devices/[id]/page.js.map +7 -0
  112. package/dist/modules/devices/backend/devices/[id]/page.meta.js +16 -0
  113. package/dist/modules/devices/backend/devices/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/devices/backend/devices/create/page.js +68 -0
  115. package/dist/modules/devices/backend/devices/create/page.js.map +7 -0
  116. package/dist/modules/devices/backend/devices/create/page.meta.js +16 -0
  117. package/dist/modules/devices/backend/devices/create/page.meta.js.map +7 -0
  118. package/dist/modules/devices/backend/devices/page.js +235 -0
  119. package/dist/modules/devices/backend/devices/page.js.map +7 -0
  120. package/dist/modules/devices/backend/devices/page.meta.js +23 -0
  121. package/dist/modules/devices/backend/devices/page.meta.js.map +7 -0
  122. package/dist/modules/devices/commands/deactivate.js +107 -0
  123. package/dist/modules/devices/commands/deactivate.js.map +7 -0
  124. package/dist/modules/devices/commands/index.js +4 -0
  125. package/dist/modules/devices/commands/index.js.map +7 -0
  126. package/dist/modules/devices/commands/register.js +183 -0
  127. package/dist/modules/devices/commands/register.js.map +7 -0
  128. package/dist/modules/devices/commands/shared.js +74 -0
  129. package/dist/modules/devices/commands/shared.js.map +7 -0
  130. package/dist/modules/devices/commands/update.js +126 -0
  131. package/dist/modules/devices/commands/update.js.map +7 -0
  132. package/dist/modules/devices/data/entities.js +83 -0
  133. package/dist/modules/devices/data/entities.js.map +7 -0
  134. package/dist/modules/devices/data/validators.js +49 -0
  135. package/dist/modules/devices/data/validators.js.map +7 -0
  136. package/dist/modules/devices/di.js +11 -0
  137. package/dist/modules/devices/di.js.map +7 -0
  138. package/dist/modules/devices/encryption.js +12 -0
  139. package/dist/modules/devices/encryption.js.map +7 -0
  140. package/dist/modules/devices/events.js +19 -0
  141. package/dist/modules/devices/events.js.map +7 -0
  142. package/dist/modules/devices/index.js +17 -0
  143. package/dist/modules/devices/index.js.map +7 -0
  144. package/dist/modules/devices/lib/operationMetadata.js +20 -0
  145. package/dist/modules/devices/lib/operationMetadata.js.map +7 -0
  146. package/dist/modules/devices/migrations/Migration20260602120000.js +15 -0
  147. package/dist/modules/devices/migrations/Migration20260602120000.js.map +7 -0
  148. package/dist/modules/devices/migrations/Migration20260630120000.js +15 -0
  149. package/dist/modules/devices/migrations/Migration20260630120000.js.map +7 -0
  150. package/dist/modules/devices/migrations/Migration20260701120000.js +13 -0
  151. package/dist/modules/devices/migrations/Migration20260701120000.js.map +7 -0
  152. package/dist/modules/devices/migrations/Migration20260721120000.js +15 -0
  153. package/dist/modules/devices/migrations/Migration20260721120000.js.map +7 -0
  154. package/dist/modules/devices/setup.js +13 -0
  155. package/dist/modules/devices/setup.js.map +7 -0
  156. package/dist/modules/entities/cli.js +8 -3
  157. package/dist/modules/entities/cli.js.map +2 -2
  158. package/dist/modules/entities/migrations/Migration20260722120000.js +28 -0
  159. package/dist/modules/entities/migrations/Migration20260722120000.js.map +7 -0
  160. package/dist/modules/inbox_ops/notifications.js +2 -0
  161. package/dist/modules/inbox_ops/notifications.js.map +2 -2
  162. package/dist/modules/messages/notifications.js +2 -0
  163. package/dist/modules/messages/notifications.js.map +2 -2
  164. package/dist/modules/notifications/acl.js +3 -1
  165. package/dist/modules/notifications/acl.js.map +2 -2
  166. package/dist/modules/notifications/api/admin/preferences/route.js +167 -0
  167. package/dist/modules/notifications/api/admin/preferences/route.js.map +7 -0
  168. package/dist/modules/notifications/api/channels/route.js +54 -0
  169. package/dist/modules/notifications/api/channels/route.js.map +7 -0
  170. package/dist/modules/notifications/api/preferences/route.js +126 -0
  171. package/dist/modules/notifications/api/preferences/route.js.map +7 -0
  172. package/dist/modules/notifications/api/route.js +8 -1
  173. package/dist/modules/notifications/api/route.js.map +2 -2
  174. package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js +187 -0
  175. package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js.map +7 -0
  176. package/dist/modules/notifications/api/types/route.js +225 -0
  177. package/dist/modules/notifications/api/types/route.js.map +7 -0
  178. package/dist/modules/notifications/api/unread-count/route.js +5 -1
  179. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  180. package/dist/modules/notifications/backend/notifications/user-preferences/page.js +10 -0
  181. package/dist/modules/notifications/backend/notifications/user-preferences/page.js.map +7 -0
  182. package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js +28 -0
  183. package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js.map +7 -0
  184. package/dist/modules/notifications/backend/profile/notification-preferences/page.js +10 -0
  185. package/dist/modules/notifications/backend/profile/notification-preferences/page.js.map +7 -0
  186. package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +33 -0
  187. package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +7 -0
  188. package/dist/modules/notifications/data/entities.js +128 -1
  189. package/dist/modules/notifications/data/entities.js.map +2 -2
  190. package/dist/modules/notifications/data/validators.js +95 -2
  191. package/dist/modules/notifications/data/validators.js.map +2 -2
  192. package/dist/modules/notifications/di.js +7 -0
  193. package/dist/modules/notifications/di.js.map +2 -2
  194. package/dist/modules/notifications/events.js +17 -0
  195. package/dist/modules/notifications/events.js.map +2 -2
  196. package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js +190 -0
  197. package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js.map +7 -0
  198. package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js +124 -0
  199. package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js.map +7 -0
  200. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +166 -1
  201. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  202. package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js +172 -0
  203. package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js.map +7 -0
  204. package/dist/modules/notifications/generators.js +63 -0
  205. package/dist/modules/notifications/generators.js.map +7 -0
  206. package/dist/modules/notifications/lib/delivery-strategies-registry.js +19 -0
  207. package/dist/modules/notifications/lib/delivery-strategies-registry.js.map +7 -0
  208. package/dist/modules/notifications/lib/deliveryStrategies.js.map +2 -2
  209. package/dist/modules/notifications/lib/derive-category.js +8 -0
  210. package/dist/modules/notifications/lib/derive-category.js.map +7 -0
  211. package/dist/modules/notifications/lib/notification-channel-registry.js +35 -0
  212. package/dist/modules/notifications/lib/notification-channel-registry.js.map +7 -0
  213. package/dist/modules/notifications/lib/notification-type-registry.js +87 -0
  214. package/dist/modules/notifications/lib/notification-type-registry.js.map +7 -0
  215. package/dist/modules/notifications/lib/notificationCopy.js +14 -0
  216. package/dist/modules/notifications/lib/notificationCopy.js.map +7 -0
  217. package/dist/modules/notifications/lib/notificationFactory.js +4 -1
  218. package/dist/modules/notifications/lib/notificationFactory.js.map +2 -2
  219. package/dist/modules/notifications/lib/notificationMapper.js +2 -0
  220. package/dist/modules/notifications/lib/notificationMapper.js.map +2 -2
  221. package/dist/modules/notifications/lib/notificationPreferenceService.js +83 -0
  222. package/dist/modules/notifications/lib/notificationPreferenceService.js.map +7 -0
  223. package/dist/modules/notifications/lib/notificationService.js +83 -28
  224. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  225. package/dist/modules/notifications/lib/notificationVisibility.js +24 -0
  226. package/dist/modules/notifications/lib/notificationVisibility.js.map +7 -0
  227. package/dist/modules/notifications/lib/routeHelpers.js +11 -1
  228. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  229. package/dist/modules/notifications/lib/shouldDeliver.js +46 -0
  230. package/dist/modules/notifications/lib/shouldDeliver.js.map +7 -0
  231. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +54 -0
  232. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +7 -0
  233. package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js +12 -0
  234. package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js.map +7 -0
  235. package/dist/modules/notifications/lib/typeCatalogue.js +71 -0
  236. package/dist/modules/notifications/lib/typeCatalogue.js.map +7 -0
  237. package/dist/modules/notifications/lib/typeChannelSettings.js +12 -0
  238. package/dist/modules/notifications/lib/typeChannelSettings.js.map +7 -0
  239. package/dist/modules/notifications/lib/typeOverrides.js +19 -0
  240. package/dist/modules/notifications/lib/typeOverrides.js.map +7 -0
  241. package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js +14 -0
  242. package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js.map +7 -0
  243. package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js +15 -0
  244. package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js.map +7 -0
  245. package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js +13 -0
  246. package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js.map +7 -0
  247. package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js +15 -0
  248. package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js.map +7 -0
  249. package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js +13 -0
  250. package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js.map +7 -0
  251. package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js +14 -0
  252. package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js.map +7 -0
  253. package/dist/modules/notifications/notification-channels.js +26 -0
  254. package/dist/modules/notifications/notification-channels.js.map +7 -0
  255. package/dist/modules/notifications/notifications.delivery-strategies.js +12 -0
  256. package/dist/modules/notifications/notifications.delivery-strategies.js.map +7 -0
  257. package/dist/modules/notifications/setup.js +16 -1
  258. package/dist/modules/notifications/setup.js.map +2 -2
  259. package/dist/modules/notifications/subscribers/deliver-notification.js +66 -57
  260. package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
  261. package/dist/modules/notifications/subscribers/sync-notification-types.js +16 -0
  262. package/dist/modules/notifications/subscribers/sync-notification-types.js.map +7 -0
  263. package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js +23 -0
  264. package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js.map +7 -0
  265. package/dist/modules/notifications/widgets/injection-table.js +14 -0
  266. package/dist/modules/notifications/widgets/injection-table.js.map +7 -0
  267. package/dist/modules/push_notifications/acl.js +13 -0
  268. package/dist/modules/push_notifications/acl.js.map +7 -0
  269. package/dist/modules/push_notifications/api/custom-send/route.js +141 -0
  270. package/dist/modules/push_notifications/api/custom-send/route.js.map +7 -0
  271. package/dist/modules/push_notifications/api/deliveries/[id]/route.js +110 -0
  272. package/dist/modules/push_notifications/api/deliveries/[id]/route.js.map +7 -0
  273. package/dist/modules/push_notifications/api/deliveries/route.js +56 -0
  274. package/dist/modules/push_notifications/api/deliveries/route.js.map +7 -0
  275. package/dist/modules/push_notifications/api/openapi.js +19 -0
  276. package/dist/modules/push_notifications/api/openapi.js.map +7 -0
  277. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js +103 -0
  278. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js.map +7 -0
  279. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js +13 -0
  280. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js.map +7 -0
  281. package/dist/modules/push_notifications/backend/push_notifications/page.js +213 -0
  282. package/dist/modules/push_notifications/backend/push_notifications/page.js.map +7 -0
  283. package/dist/modules/push_notifications/backend/push_notifications/page.meta.js +23 -0
  284. package/dist/modules/push_notifications/backend/push_notifications/page.meta.js.map +7 -0
  285. package/dist/modules/push_notifications/backend/push_notifications/send/page.js +245 -0
  286. package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +7 -0
  287. package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js +26 -0
  288. package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js.map +7 -0
  289. package/dist/modules/push_notifications/data/entities.js +92 -0
  290. package/dist/modules/push_notifications/data/entities.js.map +7 -0
  291. package/dist/modules/push_notifications/data/extensions.js +23 -0
  292. package/dist/modules/push_notifications/data/extensions.js.map +7 -0
  293. package/dist/modules/push_notifications/data/validators.js +89 -0
  294. package/dist/modules/push_notifications/data/validators.js.map +7 -0
  295. package/dist/modules/push_notifications/di.js +17 -0
  296. package/dist/modules/push_notifications/di.js.map +7 -0
  297. package/dist/modules/push_notifications/events.js +24 -0
  298. package/dist/modules/push_notifications/events.js.map +7 -0
  299. package/dist/modules/push_notifications/index.js +16 -0
  300. package/dist/modules/push_notifications/index.js.map +7 -0
  301. package/dist/modules/push_notifications/lib/fake-provider-recorder.js +80 -0
  302. package/dist/modules/push_notifications/lib/fake-provider-recorder.js.map +7 -0
  303. package/dist/modules/push_notifications/lib/push-delivery-strategy.js +54 -0
  304. package/dist/modules/push_notifications/lib/push-delivery-strategy.js.map +7 -0
  305. package/dist/modules/push_notifications/lib/push-delivery.js +297 -0
  306. package/dist/modules/push_notifications/lib/push-delivery.js.map +7 -0
  307. package/dist/modules/push_notifications/lib/push-fanout.js +138 -0
  308. package/dist/modules/push_notifications/lib/push-fanout.js.map +7 -0
  309. package/dist/modules/push_notifications/lib/push-health.js +32 -0
  310. package/dist/modules/push_notifications/lib/push-health.js.map +7 -0
  311. package/dist/modules/push_notifications/lib/push-reaper.js +87 -0
  312. package/dist/modules/push_notifications/lib/push-reaper.js.map +7 -0
  313. package/dist/modules/push_notifications/lib/push-receipt-reaper.js +136 -0
  314. package/dist/modules/push_notifications/lib/push-receipt-reaper.js.map +7 -0
  315. package/dist/modules/push_notifications/lib/push-stub-adapter.js +63 -0
  316. package/dist/modules/push_notifications/lib/push-stub-adapter.js.map +7 -0
  317. package/dist/modules/push_notifications/lib/queue.js +53 -0
  318. package/dist/modules/push_notifications/lib/queue.js.map +7 -0
  319. package/dist/modules/push_notifications/lib/reclaim-window.js +13 -0
  320. package/dist/modules/push_notifications/lib/reclaim-window.js.map +7 -0
  321. package/dist/modules/push_notifications/lib/send-custom-push.js +48 -0
  322. package/dist/modules/push_notifications/lib/send-custom-push.js.map +7 -0
  323. package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js +16 -0
  324. package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js.map +7 -0
  325. package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js +13 -0
  326. package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js.map +7 -0
  327. package/dist/modules/push_notifications/notifications.delivery-strategies.js +8 -0
  328. package/dist/modules/push_notifications/notifications.delivery-strategies.js.map +7 -0
  329. package/dist/modules/push_notifications/notifications.js +40 -0
  330. package/dist/modules/push_notifications/notifications.js.map +7 -0
  331. package/dist/modules/push_notifications/setup.js +52 -0
  332. package/dist/modules/push_notifications/setup.js.map +7 -0
  333. package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js +57 -0
  334. package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js.map +7 -0
  335. package/dist/modules/push_notifications/workers/send-push.worker.js +27 -0
  336. package/dist/modules/push_notifications/workers/send-push.worker.js.map +7 -0
  337. package/dist/modules/sales/notifications.js +8 -0
  338. package/dist/modules/sales/notifications.js.map +2 -2
  339. package/dist/modules/staff/notifications.js +6 -0
  340. package/dist/modules/staff/notifications.js.map +2 -2
  341. package/dist/modules/workflows/notifications.js +2 -0
  342. package/dist/modules/workflows/notifications.js.map +2 -2
  343. package/generated/entities/notification/index.ts +3 -0
  344. package/generated/entities/notification_preference/index.ts +8 -0
  345. package/generated/entities/notification_type/index.ts +9 -0
  346. package/generated/entities/notification_type_override/index.ts +7 -0
  347. package/generated/entities/push_notification_delivery/index.ts +19 -0
  348. package/generated/entities/user_device/index.ts +16 -0
  349. package/generated/entities.ids.generated.ts +12 -1
  350. package/generated/entity-fields-registry.ts +72 -0
  351. package/package.json +7 -7
  352. package/src/helpers/integration/api.ts +2 -1
  353. package/src/helpers/integration/appRoot.ts +13 -0
  354. package/src/helpers/integration/dbFixtures.ts +1 -5
  355. package/src/helpers/integration/pushFake.ts +219 -0
  356. package/src/modules/auth/i18n/de.json +1 -0
  357. package/src/modules/auth/i18n/en.json +1 -0
  358. package/src/modules/auth/i18n/es.json +1 -0
  359. package/src/modules/auth/i18n/ko.json +1 -0
  360. package/src/modules/auth/i18n/pl.json +1 -0
  361. package/src/modules/auth/notifications.ts +16 -0
  362. package/src/modules/business_rules/i18n/de.json +2 -1
  363. package/src/modules/business_rules/i18n/en.json +2 -1
  364. package/src/modules/business_rules/i18n/es.json +2 -1
  365. package/src/modules/business_rules/i18n/ko.json +2 -1
  366. package/src/modules/business_rules/i18n/pl.json +2 -1
  367. package/src/modules/business_rules/notifications.ts +2 -0
  368. package/src/modules/catalog/i18n/de.json +2 -1
  369. package/src/modules/catalog/i18n/en.json +2 -1
  370. package/src/modules/catalog/i18n/es.json +2 -1
  371. package/src/modules/catalog/i18n/ko.json +2 -1
  372. package/src/modules/catalog/i18n/pl.json +2 -1
  373. package/src/modules/catalog/notifications.ts +2 -0
  374. package/src/modules/communication_channels/acl.ts +9 -0
  375. package/src/modules/communication_channels/api/delete/admin/channels/[id]/route.ts +168 -0
  376. package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +6 -2
  377. package/src/modules/communication_channels/api/get/channels/[id]/route.ts +6 -2
  378. package/src/modules/communication_channels/api/get/channels/route.ts +2 -1
  379. package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +16 -5
  380. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +37 -1
  381. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +175 -0
  382. package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +11 -1
  383. package/src/modules/communication_channels/commands/admin-delete-channel.ts +203 -0
  384. package/src/modules/communication_channels/commands/connect-credential-channel.ts +63 -15
  385. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +10 -2
  386. package/src/modules/communication_channels/commands/push-register.ts +7 -1
  387. package/src/modules/communication_channels/commands/push-unregister.ts +14 -3
  388. package/src/modules/communication_channels/data/entities.ts +10 -0
  389. package/src/modules/communication_channels/data/validators.ts +1 -0
  390. package/src/modules/communication_channels/i18n/de.json +35 -1
  391. package/src/modules/communication_channels/i18n/en.json +35 -1
  392. package/src/modules/communication_channels/i18n/es.json +35 -1
  393. package/src/modules/communication_channels/i18n/ko.json +35 -1
  394. package/src/modules/communication_channels/i18n/pl.json +35 -1
  395. package/src/modules/communication_channels/lib/access-control.ts +42 -0
  396. package/src/modules/communication_channels/lib/adapter.ts +25 -0
  397. package/src/modules/communication_channels/lib/connect-channel.ts +61 -26
  398. package/src/modules/communication_channels/lib/push-adapter.ts +139 -0
  399. package/src/modules/communication_channels/lib/push-capabilities.ts +42 -0
  400. package/src/modules/communication_channels/lib/push-connect-error.ts +52 -0
  401. package/src/modules/communication_channels/lib/push-credential-errors.ts +58 -0
  402. package/src/modules/communication_channels/lib/push-envelope.ts +104 -0
  403. package/src/modules/communication_channels/lib/refcounted-client-cache.ts +123 -0
  404. package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +9 -0
  405. package/src/modules/communication_channels/migrations/Migration20260703123630_communication_channels.ts +20 -0
  406. package/src/modules/communication_channels/notifications.ts +4 -0
  407. package/src/modules/communication_channels/setup.ts +2 -0
  408. package/src/modules/configs/i18n/de.json +5 -1
  409. package/src/modules/configs/i18n/en.json +5 -1
  410. package/src/modules/configs/i18n/es.json +5 -1
  411. package/src/modules/configs/i18n/ko.json +5 -1
  412. package/src/modules/configs/i18n/pl.json +5 -1
  413. package/src/modules/configs/lib/upgrade-actions.ts +21 -0
  414. package/src/modules/customer_accounts/i18n/de.json +2 -1
  415. package/src/modules/customer_accounts/i18n/en.json +2 -1
  416. package/src/modules/customer_accounts/i18n/es.json +2 -1
  417. package/src/modules/customer_accounts/i18n/ko.json +2 -1
  418. package/src/modules/customer_accounts/i18n/pl.json +2 -1
  419. package/src/modules/customer_accounts/notifications.ts +12 -0
  420. package/src/modules/customers/i18n/de.json +2 -1
  421. package/src/modules/customers/i18n/en.json +2 -1
  422. package/src/modules/customers/i18n/es.json +2 -1
  423. package/src/modules/customers/i18n/ko.json +2 -1
  424. package/src/modules/customers/i18n/pl.json +2 -1
  425. package/src/modules/customers/notifications.ts +4 -0
  426. package/src/modules/devices/AGENTS.md +101 -0
  427. package/src/modules/devices/acl.ts +7 -0
  428. package/src/modules/devices/api/[id]/route.ts +180 -0
  429. package/src/modules/devices/api/admin/devices/[id]/route.ts +235 -0
  430. package/src/modules/devices/api/admin/devices/route.ts +160 -0
  431. package/src/modules/devices/api/auth.ts +13 -0
  432. package/src/modules/devices/api/deviceList.ts +54 -0
  433. package/src/modules/devices/api/deviceOps.ts +186 -0
  434. package/src/modules/devices/api/openapi.ts +23 -0
  435. package/src/modules/devices/api/route.ts +165 -0
  436. package/src/modules/devices/backend/devices/[id]/page.meta.ts +12 -0
  437. package/src/modules/devices/backend/devices/[id]/page.tsx +164 -0
  438. package/src/modules/devices/backend/devices/create/page.meta.ts +12 -0
  439. package/src/modules/devices/backend/devices/create/page.tsx +81 -0
  440. package/src/modules/devices/backend/devices/page.meta.ts +21 -0
  441. package/src/modules/devices/backend/devices/page.tsx +259 -0
  442. package/src/modules/devices/commands/deactivate.ts +126 -0
  443. package/src/modules/devices/commands/index.ts +3 -0
  444. package/src/modules/devices/commands/register.ts +199 -0
  445. package/src/modules/devices/commands/shared.ts +119 -0
  446. package/src/modules/devices/commands/update.ts +142 -0
  447. package/src/modules/devices/data/entities.ts +79 -0
  448. package/src/modules/devices/data/validators.ts +65 -0
  449. package/src/modules/devices/di.ts +9 -0
  450. package/src/modules/devices/encryption.ts +15 -0
  451. package/src/modules/devices/events.ts +16 -0
  452. package/src/modules/devices/i18n/de.json +45 -0
  453. package/src/modules/devices/i18n/en.json +45 -0
  454. package/src/modules/devices/i18n/es.json +45 -0
  455. package/src/modules/devices/i18n/ko.json +45 -0
  456. package/src/modules/devices/i18n/pl.json +45 -0
  457. package/src/modules/devices/index.ts +15 -0
  458. package/src/modules/devices/lib/operationMetadata.ts +35 -0
  459. package/src/modules/devices/migrations/.snapshot-open-mercato.json +200 -0
  460. package/src/modules/devices/migrations/Migration20260602120000.ts +15 -0
  461. package/src/modules/devices/migrations/Migration20260630120000.ts +20 -0
  462. package/src/modules/devices/migrations/Migration20260701120000.ts +13 -0
  463. package/src/modules/devices/migrations/Migration20260721120000.ts +18 -0
  464. package/src/modules/devices/setup.ts +11 -0
  465. package/src/modules/entities/cli.ts +16 -2
  466. package/src/modules/entities/migrations/Migration20260722120000.ts +38 -0
  467. package/src/modules/inbox_ops/i18n/de.json +2 -1
  468. package/src/modules/inbox_ops/i18n/en.json +2 -1
  469. package/src/modules/inbox_ops/i18n/es.json +2 -1
  470. package/src/modules/inbox_ops/i18n/ko.json +2 -1
  471. package/src/modules/inbox_ops/i18n/pl.json +2 -1
  472. package/src/modules/inbox_ops/notifications.ts +2 -0
  473. package/src/modules/messages/i18n/de.json +2 -1
  474. package/src/modules/messages/i18n/en.json +2 -1
  475. package/src/modules/messages/i18n/es.json +2 -1
  476. package/src/modules/messages/i18n/ko.json +2 -1
  477. package/src/modules/messages/i18n/pl.json +2 -1
  478. package/src/modules/messages/notifications.ts +2 -0
  479. package/src/modules/notifications/AGENTS.md +54 -0
  480. package/src/modules/notifications/acl.ts +2 -0
  481. package/src/modules/notifications/api/admin/preferences/route.ts +193 -0
  482. package/src/modules/notifications/api/channels/route.ts +54 -0
  483. package/src/modules/notifications/api/preferences/route.ts +131 -0
  484. package/src/modules/notifications/api/route.ts +8 -1
  485. package/src/modules/notifications/api/types/[id]/channels/[channel]/route.ts +226 -0
  486. package/src/modules/notifications/api/types/route.ts +239 -0
  487. package/src/modules/notifications/api/unread-count/route.ts +5 -1
  488. package/src/modules/notifications/backend/notifications/user-preferences/page.meta.ts +26 -0
  489. package/src/modules/notifications/backend/notifications/user-preferences/page.tsx +12 -0
  490. package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +31 -0
  491. package/src/modules/notifications/backend/profile/notification-preferences/page.tsx +12 -0
  492. package/src/modules/notifications/data/entities.ts +144 -0
  493. package/src/modules/notifications/data/validators.ts +116 -0
  494. package/src/modules/notifications/di.ts +7 -0
  495. package/src/modules/notifications/events.ts +23 -0
  496. package/src/modules/notifications/frontend/NotificationPreferenceMatrix.tsx +287 -0
  497. package/src/modules/notifications/frontend/NotificationPreferencesPageClient.tsx +155 -0
  498. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +273 -2
  499. package/src/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.tsx +214 -0
  500. package/src/modules/notifications/generators.ts +82 -0
  501. package/src/modules/notifications/i18n/de.json +43 -0
  502. package/src/modules/notifications/i18n/en.json +43 -0
  503. package/src/modules/notifications/i18n/es.json +43 -0
  504. package/src/modules/notifications/i18n/ko.json +43 -0
  505. package/src/modules/notifications/i18n/pl.json +44 -1
  506. package/src/modules/notifications/lib/delivery-strategies-registry.ts +30 -0
  507. package/src/modules/notifications/lib/deliveryStrategies.ts +40 -4
  508. package/src/modules/notifications/lib/derive-category.ts +15 -0
  509. package/src/modules/notifications/lib/notification-channel-registry.ts +59 -0
  510. package/src/modules/notifications/lib/notification-type-registry.ts +182 -0
  511. package/src/modules/notifications/lib/notificationCopy.ts +34 -0
  512. package/src/modules/notifications/lib/notificationFactory.ts +5 -1
  513. package/src/modules/notifications/lib/notificationMapper.ts +2 -0
  514. package/src/modules/notifications/lib/notificationPreferenceService.ts +138 -0
  515. package/src/modules/notifications/lib/notificationService.ts +129 -25
  516. package/src/modules/notifications/lib/notificationVisibility.ts +48 -0
  517. package/src/modules/notifications/lib/routeHelpers.ts +14 -1
  518. package/src/modules/notifications/lib/shouldDeliver.ts +112 -0
  519. package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +63 -0
  520. package/src/modules/notifications/lib/strategies/in-app-delivery-strategy.ts +21 -0
  521. package/src/modules/notifications/lib/typeCatalogue.ts +141 -0
  522. package/src/modules/notifications/lib/typeChannelSettings.ts +29 -0
  523. package/src/modules/notifications/lib/typeOverrides.ts +46 -0
  524. package/src/modules/notifications/migrations/.snapshot-open-mercato.json +874 -100
  525. package/src/modules/notifications/migrations/Migration20260625122947_notifications.ts +14 -0
  526. package/src/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.ts +15 -0
  527. package/src/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.ts +13 -0
  528. package/src/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.ts +15 -0
  529. package/src/modules/notifications/migrations/Migration20260706120000_notifications_channels.ts +13 -0
  530. package/src/modules/notifications/migrations/Migration20260716123121_notifications.ts +14 -0
  531. package/src/modules/notifications/notification-channels.ts +30 -0
  532. package/src/modules/notifications/notifications.delivery-strategies.ts +15 -0
  533. package/src/modules/notifications/setup.ts +23 -1
  534. package/src/modules/notifications/subscribers/deliver-notification.ts +105 -71
  535. package/src/modules/notifications/subscribers/sync-notification-types.ts +18 -0
  536. package/src/modules/notifications/widgets/injection/profile-preferences-menu/widget.ts +35 -0
  537. package/src/modules/notifications/widgets/injection-table.ts +18 -0
  538. package/src/modules/push_notifications/AGENTS.md +92 -0
  539. package/src/modules/push_notifications/acl.ts +9 -0
  540. package/src/modules/push_notifications/api/custom-send/route.ts +175 -0
  541. package/src/modules/push_notifications/api/deliveries/[id]/route.ts +122 -0
  542. package/src/modules/push_notifications/api/deliveries/route.ts +58 -0
  543. package/src/modules/push_notifications/api/openapi.ts +22 -0
  544. package/src/modules/push_notifications/backend/push_notifications/[id]/page.meta.ts +9 -0
  545. package/src/modules/push_notifications/backend/push_notifications/[id]/page.tsx +149 -0
  546. package/src/modules/push_notifications/backend/push_notifications/page.meta.ts +21 -0
  547. package/src/modules/push_notifications/backend/push_notifications/page.tsx +239 -0
  548. package/src/modules/push_notifications/backend/push_notifications/send/page.meta.ts +24 -0
  549. package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +327 -0
  550. package/src/modules/push_notifications/data/entities.ts +111 -0
  551. package/src/modules/push_notifications/data/extensions.ts +29 -0
  552. package/src/modules/push_notifications/data/validators.ts +102 -0
  553. package/src/modules/push_notifications/di.ts +20 -0
  554. package/src/modules/push_notifications/events.ts +21 -0
  555. package/src/modules/push_notifications/i18n/de.json +79 -0
  556. package/src/modules/push_notifications/i18n/en.json +79 -0
  557. package/src/modules/push_notifications/i18n/es.json +79 -0
  558. package/src/modules/push_notifications/i18n/ko.json +79 -0
  559. package/src/modules/push_notifications/i18n/pl.json +79 -0
  560. package/src/modules/push_notifications/index.ts +15 -0
  561. package/src/modules/push_notifications/lib/fake-provider-recorder.ts +154 -0
  562. package/src/modules/push_notifications/lib/push-delivery-strategy.ts +90 -0
  563. package/src/modules/push_notifications/lib/push-delivery.ts +455 -0
  564. package/src/modules/push_notifications/lib/push-fanout.ts +237 -0
  565. package/src/modules/push_notifications/lib/push-health.ts +47 -0
  566. package/src/modules/push_notifications/lib/push-reaper.ts +140 -0
  567. package/src/modules/push_notifications/lib/push-receipt-reaper.ts +210 -0
  568. package/src/modules/push_notifications/lib/push-stub-adapter.ts +80 -0
  569. package/src/modules/push_notifications/lib/queue.ts +74 -0
  570. package/src/modules/push_notifications/lib/reclaim-window.ts +16 -0
  571. package/src/modules/push_notifications/lib/send-custom-push.ts +100 -0
  572. package/src/modules/push_notifications/migrations/.snapshot-open-mercato.json +386 -0
  573. package/src/modules/push_notifications/migrations/Migration20260625150049_push_notifications.ts +16 -0
  574. package/src/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.ts +13 -0
  575. package/src/modules/push_notifications/notifications.delivery-strategies.ts +8 -0
  576. package/src/modules/push_notifications/notifications.ts +50 -0
  577. package/src/modules/push_notifications/setup.ts +92 -0
  578. package/src/modules/push_notifications/workers/reclaim-stuck.worker.ts +78 -0
  579. package/src/modules/push_notifications/workers/send-push.worker.ts +34 -0
  580. package/src/modules/sales/i18n/de.json +1 -0
  581. package/src/modules/sales/i18n/en.json +1 -0
  582. package/src/modules/sales/i18n/es.json +1 -0
  583. package/src/modules/sales/i18n/ko.json +1 -0
  584. package/src/modules/sales/i18n/pl.json +1 -0
  585. package/src/modules/sales/notifications.ts +8 -0
  586. package/src/modules/staff/i18n/de.json +1 -0
  587. package/src/modules/staff/i18n/en.json +1 -0
  588. package/src/modules/staff/i18n/es.json +1 -0
  589. package/src/modules/staff/i18n/ko.json +1 -0
  590. package/src/modules/staff/i18n/pl.json +1 -0
  591. package/src/modules/staff/notifications.ts +6 -0
  592. package/src/modules/workflows/i18n/de.json +1 -0
  593. package/src/modules/workflows/i18n/en.json +1 -0
  594. package/src/modules/workflows/i18n/es.json +1 -0
  595. package/src/modules/workflows/i18n/ko.json +1 -0
  596. package/src/modules/workflows/i18n/pl.json +1 -0
  597. package/src/modules/workflows/notifications.ts +2 -0
@@ -0,0 +1,199 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
3
+ import { ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
4
+ import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
5
+ import { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
6
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
7
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
9
+ import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
10
+ import { UserDevice } from '../data/entities'
11
+ import { registerDeviceCommandSchema, type RegisterDeviceCommandInput } from '../data/validators'
12
+ import { emitDevicesEvent } from '../events'
13
+ import {
14
+ applySnapshot,
15
+ deviceEvents,
16
+ deviceIndexer,
17
+ isDeviceUniqueViolation,
18
+ loadExistingDevice,
19
+ serializeDevice,
20
+ type DeviceSnapshot,
21
+ type DeviceUndoPayload,
22
+ } from './shared'
23
+
24
+ const registerDeviceCommand: CommandHandler<RegisterDeviceCommandInput, { id: string; deviceId: string; revived: boolean }> = {
25
+ id: 'devices.user_devices.register',
26
+ async prepare(rawInput, ctx) {
27
+ const parsed = registerDeviceCommandSchema.parse(rawInput)
28
+ // Enforce tenant scope before any DB access so prepare() can't probe across tenants.
29
+ ensureTenantScope(ctx, parsed.tenantId)
30
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
31
+ const existing = await loadExistingDevice(em, {
32
+ tenantId: parsed.tenantId,
33
+ organizationId: parsed.organizationId ?? null,
34
+ userId: parsed.userId,
35
+ deviceId: parsed.deviceId,
36
+ })
37
+ return existing ? { before: serializeDevice(existing) } : {}
38
+ },
39
+ async execute(rawInput, ctx) {
40
+ const parsed = registerDeviceCommandSchema.parse(rawInput)
41
+ ensureTenantScope(ctx, parsed.tenantId)
42
+
43
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
44
+ const existing = await loadExistingDevice(em, {
45
+ tenantId: parsed.tenantId,
46
+ organizationId: parsed.organizationId ?? null,
47
+ userId: parsed.userId,
48
+ deviceId: parsed.deviceId,
49
+ })
50
+ const now = new Date()
51
+ const hasPushToken = parsed.pushToken !== undefined && parsed.pushToken !== null
52
+ const wasActive = existing ? existing.deletedAt == null : false
53
+
54
+ let device!: UserDevice
55
+ try {
56
+ await withAtomicFlush(
57
+ em,
58
+ [
59
+ () => {
60
+ if (existing) {
61
+ device = existing
62
+ device.platform = parsed.platform
63
+ device.organizationId = parsed.organizationId ?? device.organizationId ?? null
64
+ if (parsed.clientAppVersion !== undefined) device.clientAppVersion = parsed.clientAppVersion ?? null
65
+ if (parsed.osVersion !== undefined) device.osVersion = parsed.osVersion ?? null
66
+ if (parsed.locale !== undefined) device.locale = parsed.locale ?? null
67
+ if (hasPushToken) {
68
+ device.pushToken = parsed.pushToken ?? null
69
+ device.pushProvider = parsed.pushProvider ?? device.pushProvider ?? null
70
+ device.pushTokenUpdatedAt = now
71
+ }
72
+ device.lastSeenAt = now
73
+ device.deletedAt = null
74
+ } else {
75
+ device = em.create(UserDevice, {
76
+ tenantId: parsed.tenantId,
77
+ organizationId: parsed.organizationId ?? null,
78
+ userId: parsed.userId,
79
+ deviceId: parsed.deviceId,
80
+ platform: parsed.platform,
81
+ clientAppVersion: parsed.clientAppVersion ?? null,
82
+ osVersion: parsed.osVersion ?? null,
83
+ locale: parsed.locale ?? null,
84
+ pushToken: hasPushToken ? parsed.pushToken ?? null : null,
85
+ pushProvider: hasPushToken ? parsed.pushProvider ?? null : null,
86
+ pushTokenUpdatedAt: hasPushToken ? now : null,
87
+ lastSeenAt: now,
88
+ })
89
+ em.persist(device)
90
+ }
91
+ },
92
+ ],
93
+ { transaction: true },
94
+ )
95
+ } catch (err) {
96
+ if (isDeviceUniqueViolation(err)) {
97
+ // Default English message is a fallback; device routes translate by `code` (see route POST
98
+ // handlers), matching the customers dictionaries conflict pattern.
99
+ throw new CrudHttpError(409, {
100
+ code: 'device_already_registered',
101
+ error: 'This device is already registered',
102
+ })
103
+ }
104
+ throw err
105
+ }
106
+
107
+ const revived = Boolean(existing) && !wasActive
108
+ const de = ctx.container.resolve('dataEngine') as DataEngine
109
+ await emitCrudSideEffects({
110
+ dataEngine: de,
111
+ action: existing ? 'updated' : 'created',
112
+ entity: device,
113
+ identifiers: {
114
+ id: device.id,
115
+ tenantId: device.tenantId,
116
+ organizationId: device.organizationId ?? null,
117
+ },
118
+ indexer: deviceIndexer,
119
+ events: deviceEvents,
120
+ })
121
+ await emitDevicesEvent(
122
+ 'devices.user_device.registered',
123
+ {
124
+ id: device.id,
125
+ tenantId: device.tenantId,
126
+ organizationId: device.organizationId ?? null,
127
+ userId: device.userId,
128
+ deviceId: device.deviceId,
129
+ platform: device.platform,
130
+ revived,
131
+ },
132
+ { persistent: true },
133
+ )
134
+
135
+ return { id: device.id, deviceId: device.deviceId, revived }
136
+ },
137
+ captureAfter: async (_input, result, ctx) => {
138
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
139
+ const device = await findOneWithDecryption(em, UserDevice, { id: result.id })
140
+ return device ? serializeDevice(device) : null
141
+ },
142
+ buildLog: async ({ result, snapshots }) => {
143
+ const before = (snapshots.before as DeviceSnapshot | undefined) ?? null
144
+ const after = (snapshots.after as DeviceSnapshot | undefined) ?? null
145
+ return {
146
+ actionLabel: before ? 'Update device' : 'Register device',
147
+ resourceKind: 'devices.user_device',
148
+ resourceId: result.id,
149
+ tenantId: after?.tenantId ?? before?.tenantId ?? null,
150
+ organizationId: after?.organizationId ?? before?.organizationId ?? null,
151
+ snapshotBefore: before,
152
+ snapshotAfter: after,
153
+ payload: {
154
+ undo: { before, after } satisfies DeviceUndoPayload,
155
+ },
156
+ }
157
+ },
158
+ undo: async ({ logEntry, ctx }) => {
159
+ const payload = extractUndoPayload<DeviceUndoPayload>(logEntry)
160
+ const after = payload?.after
161
+ if (!after) return
162
+ const before = payload?.before ?? null
163
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
164
+ const device = await findOneWithDecryption(em, UserDevice, { id: after.id })
165
+ if (!device) return
166
+
167
+ await withAtomicFlush(
168
+ em,
169
+ [
170
+ () => {
171
+ if (before) {
172
+ // Upsert/revive: restore the prior row state.
173
+ applySnapshot(device, before)
174
+ } else {
175
+ // Fresh registration: undo by soft-deleting the created row.
176
+ device.deletedAt = new Date()
177
+ }
178
+ },
179
+ ],
180
+ { transaction: true },
181
+ )
182
+
183
+ const de = ctx.container.resolve('dataEngine') as DataEngine
184
+ await emitCrudUndoSideEffects({
185
+ dataEngine: de,
186
+ action: before ? 'updated' : 'deleted',
187
+ entity: device,
188
+ identifiers: {
189
+ id: device.id,
190
+ tenantId: device.tenantId,
191
+ organizationId: device.organizationId ?? null,
192
+ },
193
+ indexer: deviceIndexer,
194
+ events: deviceEvents,
195
+ })
196
+ },
197
+ }
198
+
199
+ registerCommand(registerDeviceCommand)
@@ -0,0 +1,119 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { UniqueConstraintViolationException } from '@mikro-orm/core'
3
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
+ import type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
5
+ import { E } from '#generated/entities.ids.generated'
6
+ import { UserDevice } from '../data/entities'
7
+
8
+ export type DeviceSnapshot = {
9
+ id: string
10
+ tenantId: string
11
+ organizationId: string | null
12
+ userId: string
13
+ deviceId: string
14
+ platform: string
15
+ clientAppVersion: string | null
16
+ osVersion: string | null
17
+ locale: string | null
18
+ // Last-8 fingerprint of the push token — NEVER the plaintext. Audit snapshots and the undo payload
19
+ // both persist to `action_logs` (no TTL, never purged, and `persistLog` permits a null tenantId the
20
+ // encryption subscriber skips), so storing the real token there would leak a device credential
21
+ // forever. The live encrypted column is the single source of truth; undo re-reads it (see applySnapshot).
22
+ pushTokenFingerprint: string | null
23
+ pushProvider: string | null
24
+ pushTokenUpdatedAt: string | null
25
+ lastSeenAt: string
26
+ deletedAt: string | null
27
+ }
28
+
29
+ export type DeviceUndoPayload = {
30
+ before?: DeviceSnapshot | null
31
+ after?: DeviceSnapshot | null
32
+ }
33
+
34
+ export const deviceIndexer: CrudIndexerConfig<UserDevice> = {
35
+ entityType: E.devices.user_device,
36
+ }
37
+
38
+ export const deviceEvents: CrudEventsConfig<UserDevice> = {
39
+ module: 'devices',
40
+ entity: 'user_device',
41
+ persistent: true,
42
+ buildPayload: (ctx) => ({
43
+ id: ctx.identifiers.id,
44
+ tenantId: ctx.identifiers.tenantId,
45
+ organizationId: ctx.identifiers.organizationId,
46
+ }),
47
+ }
48
+
49
+ function toDate(value: string | null | undefined): Date | null {
50
+ if (!value) return null
51
+ const date = new Date(value)
52
+ return Number.isNaN(date.getTime()) ? null : date
53
+ }
54
+
55
+ // Persist at most the last 8 chars of the (long) provider token — never the full secret. Mirrors the
56
+ // delivery log's `token_snapshot` (push-fanout.ts) so audit/undo carry a stable, non-sensitive fingerprint.
57
+ function fingerprintPushToken(token: string | null): string | null {
58
+ return token ? token.slice(-8) : null
59
+ }
60
+
61
+ export function serializeDevice(device: UserDevice): DeviceSnapshot {
62
+ return {
63
+ id: device.id,
64
+ tenantId: device.tenantId,
65
+ organizationId: device.organizationId ?? null,
66
+ userId: device.userId,
67
+ deviceId: device.deviceId,
68
+ platform: device.platform,
69
+ clientAppVersion: device.clientAppVersion ?? null,
70
+ osVersion: device.osVersion ?? null,
71
+ locale: device.locale ?? null,
72
+ pushTokenFingerprint: fingerprintPushToken(device.pushToken ?? null),
73
+ pushProvider: device.pushProvider ?? null,
74
+ pushTokenUpdatedAt: device.pushTokenUpdatedAt ? device.pushTokenUpdatedAt.toISOString() : null,
75
+ lastSeenAt: device.lastSeenAt.toISOString(),
76
+ deletedAt: device.deletedAt ? device.deletedAt.toISOString() : null,
77
+ }
78
+ }
79
+
80
+ export function applySnapshot(device: UserDevice, snapshot: DeviceSnapshot): void {
81
+ device.platform = snapshot.platform as UserDevice['platform']
82
+ device.clientAppVersion = snapshot.clientAppVersion
83
+ device.osVersion = snapshot.osVersion
84
+ device.locale = snapshot.locale
85
+ // The push credential triple (token, provider, token_updated_at) is deliberately NOT restored from a
86
+ // snapshot. The snapshot carries only a fingerprint, and the token is a device-issued credential whose
87
+ // live encrypted column is authoritative — undo preserves whatever token the device currently holds.
88
+ // Restoring a stale token, or the redacted/undecryptable placeholder a snapshot could otherwise carry,
89
+ // would unrecoverably brick delivery. Reverting metadata is safe; reverting the secret is not.
90
+ device.lastSeenAt = toDate(snapshot.lastSeenAt) ?? new Date()
91
+ device.deletedAt = toDate(snapshot.deletedAt)
92
+ }
93
+
94
+ export async function loadExistingDevice(
95
+ em: EntityManager,
96
+ // organizationId is part of the device identity (a null value matches IS NULL, mirroring the
97
+ // coalesce(...) bucket in the unique index), so a device is looked up per (tenant, org, user, device).
98
+ scope: { tenantId: string; organizationId: string | null; userId: string; deviceId: string },
99
+ ): Promise<UserDevice | null> {
100
+ // push_token is encrypted at rest; decrypt on read so snapshots/undo payloads keep the plaintext.
101
+ const dscope = { tenantId: scope.tenantId, organizationId: scope.organizationId }
102
+ const active = await findOneWithDecryption(em, UserDevice, { ...scope, deletedAt: null }, undefined, dscope)
103
+ if (active) return active
104
+ return findOneWithDecryption(em, UserDevice, scope, { orderBy: { createdAt: 'desc' } }, dscope)
105
+ }
106
+
107
+ // Postgres SQLSTATE for unique_violation. MikroORM doesn't re-export pg error codes, so name it here.
108
+ const PG_UNIQUE_VIOLATION = '23505'
109
+
110
+ // A concurrent first-registration of the same (tenant, org, user, device_id) loses the race against the
111
+ // partial unique index. Surface it as a 409 conflict instead of a raw 500 — the endpoint is an
112
+ // idempotent upsert, so the caller can simply re-issue the request to land on the existing row.
113
+ export function isDeviceUniqueViolation(error: unknown): boolean {
114
+ if (error instanceof UniqueConstraintViolationException) return true
115
+ if (!error || typeof error !== 'object') return false
116
+ if ((error as { code?: string }).code === PG_UNIQUE_VIOLATION) return true
117
+ const message = (error as { message?: string }).message
118
+ return typeof message === 'string' && message.toLowerCase().includes('duplicate key')
119
+ }
@@ -0,0 +1,142 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
3
+ import { ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
4
+ import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
5
+ import { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
6
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
7
+ import { assertFound } from '@open-mercato/shared/lib/crud/errors'
8
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
9
+ import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
10
+ import { UserDevice } from '../data/entities'
11
+ import { updateDeviceCommandSchema, type UpdateDeviceCommandInput } from '../data/validators'
12
+ import {
13
+ applySnapshot,
14
+ deviceEvents,
15
+ deviceIndexer,
16
+ serializeDevice,
17
+ type DeviceSnapshot,
18
+ type DeviceUndoPayload,
19
+ } from './shared'
20
+
21
+ const updateDeviceCommand: CommandHandler<UpdateDeviceCommandInput, { id: string }> = {
22
+ id: 'devices.user_devices.update',
23
+ async prepare(rawInput, ctx) {
24
+ const parsed = updateDeviceCommandSchema.parse(rawInput)
25
+ // Enforce tenant scope and constrain the snapshot lookup to the caller's tenant.
26
+ ensureTenantScope(ctx, parsed.tenantId)
27
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
28
+ // No fallback decryption scope: `UserDevice` always carries its own organization_id, which
29
+ // `decryptEntitiesWithFallbackScope` resolves first — matching the scope-less lookup in execute().
30
+ const device = await findOneWithDecryption(
31
+ em,
32
+ UserDevice,
33
+ { id: parsed.id, tenantId: parsed.tenantId, deletedAt: null },
34
+ )
35
+ return device ? { before: serializeDevice(device) } : {}
36
+ },
37
+ async execute(rawInput, ctx) {
38
+ const parsed = updateDeviceCommandSchema.parse(rawInput)
39
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
40
+ const device = assertFound(
41
+ await findOneWithDecryption(em, UserDevice, { id: parsed.id, deletedAt: null }),
42
+ 'Device not found',
43
+ )
44
+ ensureTenantScope(ctx, device.tenantId)
45
+
46
+ const now = new Date()
47
+ await withAtomicFlush(
48
+ em,
49
+ [
50
+ () => {
51
+ if (Object.prototype.hasOwnProperty.call(parsed, 'clientAppVersion')) {
52
+ device.clientAppVersion = parsed.clientAppVersion ?? null
53
+ }
54
+ if (Object.prototype.hasOwnProperty.call(parsed, 'osVersion')) {
55
+ device.osVersion = parsed.osVersion ?? null
56
+ }
57
+ if (Object.prototype.hasOwnProperty.call(parsed, 'locale')) {
58
+ device.locale = parsed.locale ?? null
59
+ }
60
+ if (Object.prototype.hasOwnProperty.call(parsed, 'pushToken')) {
61
+ device.pushToken = parsed.pushToken ?? null
62
+ device.pushTokenUpdatedAt = now
63
+ }
64
+ if (Object.prototype.hasOwnProperty.call(parsed, 'pushProvider')) {
65
+ device.pushProvider = parsed.pushProvider ?? null
66
+ }
67
+ // Only a client-supplied lastSeenAt advances presence. Metadata-only edits (e.g. an admin
68
+ // changing push_provider) must NOT bump last_seen_at — device presence is maintained by the
69
+ // register heartbeat, not by edits.
70
+ if (Object.prototype.hasOwnProperty.call(parsed, 'lastSeenAt') && parsed.lastSeenAt) {
71
+ device.lastSeenAt = parsed.lastSeenAt
72
+ }
73
+ },
74
+ ],
75
+ { transaction: true },
76
+ )
77
+
78
+ const de = ctx.container.resolve('dataEngine') as DataEngine
79
+ await emitCrudSideEffects({
80
+ dataEngine: de,
81
+ action: 'updated',
82
+ entity: device,
83
+ identifiers: {
84
+ id: device.id,
85
+ tenantId: device.tenantId,
86
+ organizationId: device.organizationId ?? null,
87
+ },
88
+ indexer: deviceIndexer,
89
+ events: deviceEvents,
90
+ })
91
+
92
+ return { id: device.id }
93
+ },
94
+ captureAfter: async (_input, result, ctx) => {
95
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
96
+ const device = await findOneWithDecryption(em, UserDevice, { id: result.id })
97
+ return device ? serializeDevice(device) : null
98
+ },
99
+ buildLog: async ({ snapshots }) => {
100
+ const before = (snapshots.before as DeviceSnapshot | undefined) ?? null
101
+ if (!before) return null
102
+ const after = (snapshots.after as DeviceSnapshot | undefined) ?? null
103
+ return {
104
+ actionLabel: 'Update device',
105
+ resourceKind: 'devices.user_device',
106
+ resourceId: before.id,
107
+ tenantId: before.tenantId,
108
+ organizationId: before.organizationId,
109
+ snapshotBefore: before,
110
+ snapshotAfter: after,
111
+ payload: {
112
+ undo: { before, after } satisfies DeviceUndoPayload,
113
+ },
114
+ }
115
+ },
116
+ undo: async ({ logEntry, ctx }) => {
117
+ const payload = extractUndoPayload<DeviceUndoPayload>(logEntry)
118
+ const before = payload?.before
119
+ if (!before) return
120
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
121
+ const device = await findOneWithDecryption(em, UserDevice, { id: before.id })
122
+ if (!device) return
123
+
124
+ await withAtomicFlush(em, [() => applySnapshot(device, before)], { transaction: true })
125
+
126
+ const de = ctx.container.resolve('dataEngine') as DataEngine
127
+ await emitCrudUndoSideEffects({
128
+ dataEngine: de,
129
+ action: 'updated',
130
+ entity: device,
131
+ identifiers: {
132
+ id: device.id,
133
+ tenantId: device.tenantId,
134
+ organizationId: device.organizationId ?? null,
135
+ },
136
+ indexer: deviceIndexer,
137
+ events: deviceEvents,
138
+ })
139
+ },
140
+ }
141
+
142
+ registerCommand(updateDeviceCommand)
@@ -0,0 +1,79 @@
1
+ import { OptionalProps } from '@mikro-orm/core'
2
+ import { Entity, Index, PrimaryKey, Property } from '@mikro-orm/decorators/legacy'
3
+
4
+ export type DevicePlatform = 'ios' | 'android' | 'web'
5
+
6
+ @Entity({ tableName: 'user_devices' })
7
+ @Index({ name: 'user_devices_tenant_user_idx', properties: ['tenantId', 'userId'] })
8
+ @Index({
9
+ // Device identity is scoped per organization: (tenant, org, user, device). organization_id is
10
+ // nullable, so the index is declared NULLS NOT DISTINCT — otherwise Postgres treats NULLs as
11
+ // distinct and would allow duplicate null-org rows for the same (tenant, user, device).
12
+ name: 'user_devices_tenant_org_user_device_active_unique',
13
+ expression:
14
+ 'create unique index "user_devices_tenant_org_user_device_active_unique" on "user_devices" ("tenant_id", "organization_id", "user_id", "device_id") nulls not distinct where deleted_at is null',
15
+ })
16
+ export class UserDevice {
17
+ [OptionalProps]?:
18
+ | 'organizationId'
19
+ | 'clientAppVersion'
20
+ | 'osVersion'
21
+ | 'locale'
22
+ | 'pushToken'
23
+ | 'pushProvider'
24
+ | 'pushTokenUpdatedAt'
25
+ | 'lastSeenAt'
26
+ | 'createdAt'
27
+ | 'updatedAt'
28
+ | 'deletedAt'
29
+
30
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
31
+ id!: string
32
+
33
+ @Property({ name: 'tenant_id', type: 'uuid' })
34
+ tenantId!: string
35
+
36
+ @Property({ name: 'organization_id', type: 'uuid', nullable: true })
37
+ organizationId?: string | null
38
+
39
+ @Property({ name: 'user_id', type: 'uuid' })
40
+ userId!: string
41
+
42
+ @Property({ name: 'device_id', type: 'text' })
43
+ deviceId!: string
44
+
45
+ @Property({ name: 'platform', type: 'text' })
46
+ platform!: DevicePlatform
47
+
48
+ @Property({ name: 'client_app_version', type: 'text', nullable: true })
49
+ clientAppVersion?: string | null
50
+
51
+ @Property({ name: 'os_version', type: 'text', nullable: true })
52
+ osVersion?: string | null
53
+
54
+ // Per-device language the client app picked (BCP-47, e.g. `en` / `en-US`). Normalized to a
55
+ // supported locale at push send time to translate the notification copy; null ⇒ default locale.
56
+ @Property({ name: 'locale', type: 'text', nullable: true })
57
+ locale?: string | null
58
+
59
+ @Property({ name: 'push_token', type: 'text', nullable: true })
60
+ pushToken?: string | null
61
+
62
+ @Property({ name: 'push_provider', type: 'text', nullable: true })
63
+ pushProvider?: string | null
64
+
65
+ @Property({ name: 'push_token_updated_at', type: Date, nullable: true })
66
+ pushTokenUpdatedAt?: Date | null
67
+
68
+ @Property({ name: 'last_seen_at', type: Date, onCreate: () => new Date() })
69
+ lastSeenAt: Date = new Date()
70
+
71
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
72
+ createdAt: Date = new Date()
73
+
74
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
75
+ updatedAt: Date = new Date()
76
+
77
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
78
+ deletedAt?: Date | null
79
+ }
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod'
2
+
3
+ export const devicePlatformSchema = z.enum(['ios', 'android', 'web'])
4
+ export type DevicePlatformInput = z.infer<typeof devicePlatformSchema>
5
+
6
+ // POST /api/devices — register/upsert. pushToken/pushProvider optional on first register.
7
+ export const registerDeviceSchema = z
8
+ .object({
9
+ deviceId: z.string().trim().min(1).max(255),
10
+ platform: devicePlatformSchema,
11
+ clientAppVersion: z.string().trim().max(64).nullish(),
12
+ osVersion: z.string().trim().max(64).nullish(),
13
+ locale: z.string().trim().min(2).max(35).nullish(),
14
+ pushToken: z.string().trim().min(1).max(4096).nullish(),
15
+ pushProvider: z.string().trim().min(1).max(32).nullish(),
16
+ })
17
+ .strict()
18
+ export type RegisterDeviceInput = z.infer<typeof registerDeviceSchema>
19
+
20
+ // POST /api/devices/admin/devices — admin registers a device on behalf of any user in the tenant.
21
+ // Same shape as self-register plus an explicit target userId (self-register derives it from auth).
22
+ export const registerDeviceAdminSchema = registerDeviceSchema.extend({
23
+ userId: z.string().uuid(),
24
+ })
25
+ export type RegisterDeviceAdminInput = z.infer<typeof registerDeviceAdminSchema>
26
+
27
+ // PUT /api/devices/:id — partial update.
28
+ // pushToken/pushProvider are tri-state: absent (leave unchanged) vs explicit null (clear).
29
+ // `.nullable().optional()` distinguishes `undefined` from `null`; the command relies on
30
+ // own-property presence to decide whether to touch the column.
31
+ export const updateDeviceSchema = z
32
+ .object({
33
+ clientAppVersion: z.string().trim().max(64).nullable().optional(),
34
+ osVersion: z.string().trim().max(64).nullable().optional(),
35
+ locale: z.string().trim().min(2).max(35).nullable().optional(),
36
+ pushToken: z.string().trim().min(1).max(4096).nullable().optional(),
37
+ pushProvider: z.string().trim().min(1).max(32).nullable().optional(),
38
+ lastSeenAt: z.coerce.date().optional(),
39
+ })
40
+ .strict()
41
+ export type UpdateDeviceInput = z.infer<typeof updateDeviceSchema>
42
+
43
+ // Command-side schemas append the resolved scope. Scope fields are never accepted from the client body.
44
+ export const registerDeviceCommandSchema = registerDeviceSchema.extend({
45
+ tenantId: z.string().uuid(),
46
+ organizationId: z.string().uuid().nullable().optional(),
47
+ userId: z.string().uuid(),
48
+ })
49
+ export type RegisterDeviceCommandInput = z.infer<typeof registerDeviceCommandSchema>
50
+
51
+ export const updateDeviceCommandSchema = updateDeviceSchema.extend({
52
+ id: z.string().uuid(),
53
+ tenantId: z.string().uuid(),
54
+ userId: z.string().uuid(),
55
+ organizationId: z.string().uuid().nullable().optional(),
56
+ })
57
+ export type UpdateDeviceCommandInput = z.infer<typeof updateDeviceCommandSchema>
58
+
59
+ export const deactivateDeviceCommandSchema = z.object({
60
+ id: z.string().uuid(),
61
+ tenantId: z.string().uuid(),
62
+ userId: z.string().uuid(),
63
+ organizationId: z.string().uuid().nullable().optional(),
64
+ })
65
+ export type DeactivateDeviceCommandInput = z.infer<typeof deactivateDeviceCommandSchema>
@@ -0,0 +1,9 @@
1
+ import { asValue } from 'awilix'
2
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { UserDevice } from './data/entities'
4
+
5
+ export function register(container: AppContainer) {
6
+ container.register({
7
+ UserDevice: asValue(UserDevice),
8
+ })
9
+ }
@@ -0,0 +1,15 @@
1
+ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
2
+
3
+ // `push_token` is a long-lived provider secret. It is never exposed via the API (only a last-8
4
+ // snapshot is persisted on delivery rows) and is redacted from audit-log snapshots, so it is
5
+ // encrypted at rest. Reads route through `findWithDecryption`/`findOneWithDecryption` per the
6
+ // `packages/core/AGENTS.md` Encryption section. No blind-index column is needed: the push delivery
7
+ // path looks devices up by id/user, never by token value.
8
+ export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
9
+ {
10
+ entityId: 'devices:user_device',
11
+ fields: [{ field: 'push_token' }],
12
+ },
13
+ ]
14
+
15
+ export default defaultEncryptionMaps
@@ -0,0 +1,16 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ const events = [
4
+ // Domain events (spec contract): emitted explicitly from commands.
5
+ { id: 'devices.user_device.registered', label: 'User Device Registered', entity: 'user_device', category: 'crud' },
6
+ { id: 'devices.user_device.deactivated', label: 'User Device Deactivated', entity: 'user_device', category: 'crud' },
7
+ // CRUD lifecycle events emitted by emitCrudSideEffects (keep declared to avoid undeclared-event warnings).
8
+ { id: 'devices.user_device.created', label: 'User Device Created', entity: 'user_device', category: 'crud' },
9
+ { id: 'devices.user_device.updated', label: 'User Device Updated', entity: 'user_device', category: 'crud' },
10
+ { id: 'devices.user_device.deleted', label: 'User Device Deleted', entity: 'user_device', category: 'crud' },
11
+ ] as const
12
+
13
+ export const eventsConfig = createModuleEvents({ moduleId: 'devices', events })
14
+ export const emitDevicesEvent = eventsConfig.emit
15
+ export type DevicesEventId = typeof events[number]['id']
16
+ export default eventsConfig