@cosmicdrift/kumiko-bundled-features 0.1.0

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 (333) hide show
  1. package/package.json +90 -0
  2. package/src/audit/__tests__/audit.integration.ts +328 -0
  3. package/src/audit/constants.ts +7 -0
  4. package/src/audit/feature.ts +23 -0
  5. package/src/audit/handlers/list.query.ts +98 -0
  6. package/src/audit/index.ts +2 -0
  7. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
  8. package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
  9. package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
  10. package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
  11. package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
  12. package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
  13. package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
  14. package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
  15. package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
  16. package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
  17. package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
  18. package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
  19. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
  20. package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
  21. package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
  22. package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
  23. package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
  24. package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
  25. package/src/auth-email-password/auth-user-row.ts +41 -0
  26. package/src/auth-email-password/constants.ts +101 -0
  27. package/src/auth-email-password/email-templates.ts +283 -0
  28. package/src/auth-email-password/feature.ts +140 -0
  29. package/src/auth-email-password/handlers/change-password.write.ts +58 -0
  30. package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
  31. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
  32. package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
  33. package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
  34. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
  35. package/src/auth-email-password/handlers/login.write.ts +208 -0
  36. package/src/auth-email-password/handlers/logout.write.ts +12 -0
  37. package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
  38. package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
  39. package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
  40. package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
  41. package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
  42. package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
  43. package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
  44. package/src/auth-email-password/i18n.ts +211 -0
  45. package/src/auth-email-password/identity-v3-hash.ts +97 -0
  46. package/src/auth-email-password/index.ts +35 -0
  47. package/src/auth-email-password/invite-token-store.ts +92 -0
  48. package/src/auth-email-password/lockout-store.ts +118 -0
  49. package/src/auth-email-password/password-hashing.ts +43 -0
  50. package/src/auth-email-password/reset-token.ts +28 -0
  51. package/src/auth-email-password/seeding.ts +183 -0
  52. package/src/auth-email-password/signed-token.ts +85 -0
  53. package/src/auth-email-password/signup-token-store.ts +104 -0
  54. package/src/auth-email-password/stream-tenant.ts +31 -0
  55. package/src/auth-email-password/testing.ts +5 -0
  56. package/src/auth-email-password/token-burn-store.ts +57 -0
  57. package/src/auth-email-password/verification-token.ts +27 -0
  58. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
  59. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
  60. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
  61. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
  62. package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
  63. package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
  64. package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
  65. package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
  66. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
  67. package/src/auth-email-password/web/auth-client.ts +350 -0
  68. package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
  69. package/src/auth-email-password/web/auth-gate.tsx +33 -0
  70. package/src/auth-email-password/web/client-plugin.ts +48 -0
  71. package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
  72. package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
  73. package/src/auth-email-password/web/index.ts +56 -0
  74. package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
  75. package/src/auth-email-password/web/login-screen.tsx +150 -0
  76. package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
  77. package/src/auth-email-password/web/session.tsx +171 -0
  78. package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
  79. package/src/auth-email-password/web/signup-screen.tsx +130 -0
  80. package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
  81. package/src/auth-email-password/web/use-shell-user.ts +34 -0
  82. package/src/auth-email-password/web/user-menu.tsx +89 -0
  83. package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
  84. package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
  85. package/src/billing-foundation/__tests__/feature.test.ts +110 -0
  86. package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
  87. package/src/billing-foundation/aggregate-id.ts +21 -0
  88. package/src/billing-foundation/constants.ts +70 -0
  89. package/src/billing-foundation/entities.ts +50 -0
  90. package/src/billing-foundation/events.ts +71 -0
  91. package/src/billing-foundation/feature.ts +122 -0
  92. package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
  93. package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
  94. package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
  95. package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
  96. package/src/billing-foundation/handlers/process-event.write.ts +160 -0
  97. package/src/billing-foundation/index.ts +42 -0
  98. package/src/billing-foundation/projection.ts +135 -0
  99. package/src/billing-foundation/types.ts +157 -0
  100. package/src/billing-foundation/webhook-handler.ts +184 -0
  101. package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
  102. package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
  103. package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
  104. package/src/cap-counter/aggregate-id.ts +61 -0
  105. package/src/cap-counter/constants.ts +32 -0
  106. package/src/cap-counter/enforce-cap.ts +404 -0
  107. package/src/cap-counter/entity.ts +48 -0
  108. package/src/cap-counter/feature.ts +90 -0
  109. package/src/cap-counter/handlers/get-counter.query.ts +43 -0
  110. package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
  111. package/src/cap-counter/handlers/increment.write.ts +92 -0
  112. package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
  113. package/src/cap-counter/index.ts +34 -0
  114. package/src/cap-counter/with-cap-enforcement.ts +179 -0
  115. package/src/channel-email/email-channel.ts +48 -0
  116. package/src/channel-email/feature.ts +15 -0
  117. package/src/channel-email/index.ts +4 -0
  118. package/src/channel-email/smtp-transport.ts +65 -0
  119. package/src/channel-email/types.ts +34 -0
  120. package/src/channel-in-app/constants.ts +11 -0
  121. package/src/channel-in-app/feature.ts +30 -0
  122. package/src/channel-in-app/handlers/inbox.query.ts +28 -0
  123. package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
  124. package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
  125. package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
  126. package/src/channel-in-app/in-app-channel.ts +44 -0
  127. package/src/channel-in-app/index.ts +4 -0
  128. package/src/channel-in-app/tables.ts +22 -0
  129. package/src/channel-push/feature.ts +15 -0
  130. package/src/channel-push/index.ts +3 -0
  131. package/src/channel-push/push-channel.ts +33 -0
  132. package/src/channel-push/types.ts +22 -0
  133. package/src/config/__tests__/app-overrides.test.ts +118 -0
  134. package/src/config/__tests__/config.integration.ts +1246 -0
  135. package/src/config/constants.ts +23 -0
  136. package/src/config/feature.ts +117 -0
  137. package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
  138. package/src/config/handlers/reset.write.ts +45 -0
  139. package/src/config/handlers/schema.query.ts +22 -0
  140. package/src/config/handlers/set.write.ts +93 -0
  141. package/src/config/handlers/values.query.ts +43 -0
  142. package/src/config/index.ts +15 -0
  143. package/src/config/resolver.ts +283 -0
  144. package/src/config/table.ts +35 -0
  145. package/src/config/write-helpers.ts +268 -0
  146. package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
  147. package/src/delivery/__tests__/delivery.integration.ts +1405 -0
  148. package/src/delivery/constants.ts +33 -0
  149. package/src/delivery/delivery-service.ts +489 -0
  150. package/src/delivery/events.ts +18 -0
  151. package/src/delivery/feature.ts +70 -0
  152. package/src/delivery/handlers/log.query.ts +21 -0
  153. package/src/delivery/handlers/preferences.query.ts +18 -0
  154. package/src/delivery/handlers/set-preference.write.ts +28 -0
  155. package/src/delivery/index.ts +35 -0
  156. package/src/delivery/tables.ts +74 -0
  157. package/src/delivery/testing.ts +47 -0
  158. package/src/delivery/types.ts +71 -0
  159. package/src/delivery/unsubscribe.ts +99 -0
  160. package/src/delivery/upsert-preference.ts +145 -0
  161. package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
  162. package/src/feature-toggles/constants.ts +20 -0
  163. package/src/feature-toggles/events.ts +18 -0
  164. package/src/feature-toggles/feature.ts +98 -0
  165. package/src/feature-toggles/global-feature-state-table.ts +28 -0
  166. package/src/feature-toggles/handlers/list.query.ts +26 -0
  167. package/src/feature-toggles/handlers/registered.query.ts +56 -0
  168. package/src/feature-toggles/handlers/set.write.ts +158 -0
  169. package/src/feature-toggles/index.ts +9 -0
  170. package/src/feature-toggles/toggle-runtime.ts +73 -0
  171. package/src/file-foundation/__tests__/feature.test.ts +35 -0
  172. package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
  173. package/src/file-foundation/feature.ts +123 -0
  174. package/src/file-foundation/index.ts +7 -0
  175. package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
  176. package/src/file-provider-inmemory/feature.ts +73 -0
  177. package/src/file-provider-inmemory/index.ts +3 -0
  178. package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
  179. package/src/file-provider-s3/feature.ts +169 -0
  180. package/src/file-provider-s3/index.ts +3 -0
  181. package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
  182. package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
  183. package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
  184. package/src/files-provider-s3/env-helper.ts +49 -0
  185. package/src/files-provider-s3/index.ts +3 -0
  186. package/src/files-provider-s3/s3-provider.ts +114 -0
  187. package/src/foundation-shared/config-helpers.ts +67 -0
  188. package/src/foundation-shared/index.ts +4 -0
  189. package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
  190. package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
  191. package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
  192. package/src/jobs/constants.ts +21 -0
  193. package/src/jobs/events.ts +39 -0
  194. package/src/jobs/feature.ts +150 -0
  195. package/src/jobs/handlers/detail.query.ts +30 -0
  196. package/src/jobs/handlers/list.query.ts +36 -0
  197. package/src/jobs/handlers/retry.write.ts +69 -0
  198. package/src/jobs/handlers/trigger.write.ts +39 -0
  199. package/src/jobs/index.ts +5 -0
  200. package/src/jobs/job-run-logger.ts +213 -0
  201. package/src/jobs/job-run-table.ts +55 -0
  202. package/src/legal-pages/README.md +195 -0
  203. package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
  204. package/src/legal-pages/constants.ts +36 -0
  205. package/src/legal-pages/feature.ts +187 -0
  206. package/src/legal-pages/index.ts +13 -0
  207. package/src/legal-pages/markdown.ts +69 -0
  208. package/src/mail-foundation/__tests__/feature.test.ts +46 -0
  209. package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
  210. package/src/mail-foundation/feature.ts +160 -0
  211. package/src/mail-foundation/index.ts +14 -0
  212. package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
  213. package/src/mail-transport-inmemory/feature.ts +90 -0
  214. package/src/mail-transport-inmemory/index.ts +3 -0
  215. package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
  216. package/src/mail-transport-smtp/feature.ts +182 -0
  217. package/src/mail-transport-smtp/index.ts +3 -0
  218. package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
  219. package/src/rate-limiting/constants.ts +9 -0
  220. package/src/rate-limiting/feature.ts +16 -0
  221. package/src/rate-limiting/handlers/status.query.ts +52 -0
  222. package/src/rate-limiting/index.ts +2 -0
  223. package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
  224. package/src/renderer-simple/feature.ts +12 -0
  225. package/src/renderer-simple/index.ts +2 -0
  226. package/src/renderer-simple/simple-renderer.ts +72 -0
  227. package/src/secrets/__tests__/rotate.integration.ts +176 -0
  228. package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
  229. package/src/secrets/__tests__/secrets.integration.ts +118 -0
  230. package/src/secrets/feature.ts +84 -0
  231. package/src/secrets/handlers/delete.write.ts +20 -0
  232. package/src/secrets/handlers/list.query.ts +38 -0
  233. package/src/secrets/handlers/rotate.job.ts +193 -0
  234. package/src/secrets/handlers/set.write.ts +50 -0
  235. package/src/secrets/index.ts +16 -0
  236. package/src/secrets/secrets-context.ts +296 -0
  237. package/src/secrets/table.ts +68 -0
  238. package/src/sessions/__tests__/cleanup.integration.ts +175 -0
  239. package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
  240. package/src/sessions/__tests__/sessions.integration.ts +472 -0
  241. package/src/sessions/__tests__/test-helpers.ts +66 -0
  242. package/src/sessions/constants.ts +43 -0
  243. package/src/sessions/feature.ts +84 -0
  244. package/src/sessions/handlers/cleanup.job.ts +109 -0
  245. package/src/sessions/handlers/list.query.ts +35 -0
  246. package/src/sessions/handlers/mine.query.ts +37 -0
  247. package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
  248. package/src/sessions/handlers/revoke.write.ts +76 -0
  249. package/src/sessions/index.ts +17 -0
  250. package/src/sessions/schema/index.ts +5 -0
  251. package/src/sessions/schema/user-session.ts +67 -0
  252. package/src/sessions/session-callbacks.ts +110 -0
  253. package/src/sessions/testing.ts +42 -0
  254. package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
  255. package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
  256. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
  257. package/src/subscription-mollie/constants.ts +33 -0
  258. package/src/subscription-mollie/feature.ts +144 -0
  259. package/src/subscription-mollie/index.ts +13 -0
  260. package/src/subscription-mollie/plugin-methods.ts +79 -0
  261. package/src/subscription-mollie/verify-webhook.ts +244 -0
  262. package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
  263. package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
  264. package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
  265. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
  266. package/src/subscription-stripe/constants.ts +20 -0
  267. package/src/subscription-stripe/feature.ts +120 -0
  268. package/src/subscription-stripe/index.ts +14 -0
  269. package/src/subscription-stripe/plugin-methods.ts +91 -0
  270. package/src/subscription-stripe/verify-webhook.ts +235 -0
  271. package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
  272. package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
  273. package/src/tenant/__tests__/tenant.integration.ts +347 -0
  274. package/src/tenant/command-schemas.ts +37 -0
  275. package/src/tenant/constants.ts +37 -0
  276. package/src/tenant/feature.ts +109 -0
  277. package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
  278. package/src/tenant/handlers/add-member.write.ts +53 -0
  279. package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
  280. package/src/tenant/handlers/create.write.ts +21 -0
  281. package/src/tenant/handlers/disable.write.ts +18 -0
  282. package/src/tenant/handlers/invitations.query.ts +31 -0
  283. package/src/tenant/handlers/list.query.ts +17 -0
  284. package/src/tenant/handlers/me.query.ts +17 -0
  285. package/src/tenant/handlers/members.query.ts +22 -0
  286. package/src/tenant/handlers/memberships.query.ts +24 -0
  287. package/src/tenant/handlers/remove-member.write.ts +40 -0
  288. package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
  289. package/src/tenant/handlers/update-member-roles.write.ts +54 -0
  290. package/src/tenant/handlers/update.write.ts +20 -0
  291. package/src/tenant/index.ts +12 -0
  292. package/src/tenant/invitation-table.ts +93 -0
  293. package/src/tenant/membership-table.ts +35 -0
  294. package/src/tenant/schema/index.ts +5 -0
  295. package/src/tenant/schema/tenant.ts +27 -0
  296. package/src/tenant/seeding.ts +155 -0
  297. package/src/tenant/testing.ts +8 -0
  298. package/src/text-content/README.md +190 -0
  299. package/src/text-content/__tests__/text-content.integration.ts +415 -0
  300. package/src/text-content/api.ts +92 -0
  301. package/src/text-content/constants.ts +19 -0
  302. package/src/text-content/feature.ts +29 -0
  303. package/src/text-content/handlers/by-slug.query.ts +55 -0
  304. package/src/text-content/handlers/set.write.ts +118 -0
  305. package/src/text-content/index.ts +14 -0
  306. package/src/text-content/seeding.ts +91 -0
  307. package/src/text-content/table.ts +45 -0
  308. package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
  309. package/src/tier-engine/__tests__/drift.test.ts +42 -0
  310. package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
  311. package/src/tier-engine/aggregate-id.ts +27 -0
  312. package/src/tier-engine/compose-app.ts +150 -0
  313. package/src/tier-engine/constants.ts +15 -0
  314. package/src/tier-engine/entity.ts +30 -0
  315. package/src/tier-engine/feature.ts +72 -0
  316. package/src/tier-engine/handlers/active-tier.query.ts +23 -0
  317. package/src/tier-engine/index.ts +22 -0
  318. package/src/user/__tests__/seed-testing.integration.ts +127 -0
  319. package/src/user/__tests__/user.integration.ts +198 -0
  320. package/src/user/command-schemas.ts +15 -0
  321. package/src/user/constants.ts +23 -0
  322. package/src/user/feature.ts +32 -0
  323. package/src/user/handlers/create.write.ts +54 -0
  324. package/src/user/handlers/detail.query.ts +9 -0
  325. package/src/user/handlers/find-for-auth.query.ts +38 -0
  326. package/src/user/handlers/list.query.ts +8 -0
  327. package/src/user/handlers/me.query.ts +15 -0
  328. package/src/user/handlers/update.write.ts +54 -0
  329. package/src/user/index.ts +4 -0
  330. package/src/user/schema/index.ts +5 -0
  331. package/src/user/schema/user.ts +69 -0
  332. package/src/user/seeding.ts +93 -0
  333. package/src/user/testing.ts +5 -0
@@ -0,0 +1,35 @@
1
+ export type { DeliveryStatusValue } from "./constants";
2
+ export {
3
+ DELIVERY_FEATURE,
4
+ DeliveryErrors,
5
+ DeliveryHandlers,
6
+ DeliveryQueries,
7
+ DeliveryStatus,
8
+ } from "./constants";
9
+ export {
10
+ collectChannels,
11
+ collectRenderers,
12
+ createDeliveryService,
13
+ type DeliveryServiceOptions,
14
+ type KillSwitchResolver,
15
+ type RateLimitConfig,
16
+ } from "./delivery-service";
17
+ export { createDeliveryFeature } from "./feature";
18
+ export { deliveryAttemptsTable, notificationPreferencesTable } from "./tables";
19
+ export { type CreateDeliveryTestContextOptions, createDeliveryTestContext } from "./testing";
20
+ export type {
21
+ ChannelContext,
22
+ ChannelMessage,
23
+ ChannelResult,
24
+ DeliveryChannel,
25
+ DeliveryLogEntry,
26
+ DeliveryService,
27
+ NotificationRenderer,
28
+ RendererInput,
29
+ } from "./types";
30
+ export {
31
+ createUnsubscribeRoute,
32
+ signUnsubscribeToken,
33
+ type UnsubscribeRouteOptions,
34
+ type UnsubscribeTokenPayload,
35
+ } from "./unsubscribe";
@@ -0,0 +1,74 @@
1
+ import {
2
+ boolean,
3
+ buildBaseColumns,
4
+ instant,
5
+ table as pgTable,
6
+ text,
7
+ uniqueIndex,
8
+ uuid,
9
+ } from "@cosmicdrift/kumiko-framework/db";
10
+ import {
11
+ createBooleanField,
12
+ createEntity,
13
+ createTextField,
14
+ } from "@cosmicdrift/kumiko-framework/engine";
15
+ import { sql } from "drizzle-orm";
16
+
17
+ // Delivery-log is an append-only stream of per-attempt records. The stream
18
+ // of truth lives in the events-Tabelle (one aggregate per attempt, event
19
+ // type `delivery:event:attempt`). An INLINE projection materialises each
20
+ // event into a row of deliveryAttemptsTable for the log-query handler —
21
+ // same TX as the event-append, so callers can read their own writes
22
+ // synchronously. No r.entity is registered for `deliveryAttempt`: the
23
+ // boot-validator accepts events-only projection sources as long as every
24
+ // apply-key is a registered domain-event (see registry.ts).
25
+ //
26
+ // PK = event aggregate-id (uuid). Keeps the projection row linked back to
27
+ // its event stream 1:1 — same convention as jobRunsTable + tenantSecretsTable.
28
+ // Event replays stay idempotent (primary-key conflict instead of duplicate rows).
29
+ export const deliveryAttemptsTable = pgTable("read_delivery_attempts", {
30
+ id: uuid("id").primaryKey(),
31
+ tenantId: uuid("tenant_id").notNull(),
32
+ notificationType: text("notification_type").notNull(),
33
+ channel: text("channel").notNull(),
34
+ // User-IDs as UUID-strings post-ES migration.
35
+ recipientId: text("recipient_id"),
36
+ recipientAddress: text("recipient_address"),
37
+ status: text("status").notNull().$type<"sent" | "failed" | "skipped">(),
38
+ error: text("error"),
39
+ createdAt: instant("created_at").default(sql`now()`).notNull(),
40
+ });
41
+
42
+ // User-scoped opt-in/opt-out for (notificationType, channel) pairs. Post-ES
43
+ // refactor: each row is a notificationPreference aggregate with
44
+ // `.created / .updated / .deleted` lifecycle events written via the
45
+ // event-store executor. The unique index on (tenant, user, type, channel)
46
+ // is the effective natural key; the uuid PK is the aggregate id.
47
+ export const notificationPreferenceEntity = createEntity({
48
+ table: "read_notification_preferences",
49
+ fields: {
50
+ userId: createTextField({ required: true }),
51
+ notificationType: createTextField({ required: true }), // qualified name or "*"
52
+ channel: createTextField({ required: true }), // "inApp", "email", "push", or "*"
53
+ enabled: createBooleanField({ default: true }),
54
+ },
55
+ });
56
+
57
+ export const notificationPreferencesTable = pgTable(
58
+ "read_notification_preferences",
59
+ {
60
+ ...buildBaseColumns(false, "uuid"),
61
+ userId: text("user_id").notNull(),
62
+ notificationType: text("notification_type").notNull(),
63
+ channel: text("channel").notNull(),
64
+ enabled: boolean("enabled").default(true).notNull(),
65
+ },
66
+ (table) => [
67
+ uniqueIndex("read_notification_preferences_unique").on(
68
+ table.tenantId,
69
+ table.userId,
70
+ table.notificationType,
71
+ table.channel,
72
+ ),
73
+ ],
74
+ );
@@ -0,0 +1,47 @@
1
+ import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { Redis } from "ioredis";
5
+ import type { KillSwitchResolver, RateLimitConfig } from "./delivery-service";
6
+ import { collectChannels, createDeliveryService } from "./delivery-service";
7
+ import type { DeliveryService } from "./types";
8
+
9
+ export type CreateDeliveryTestContextOptions = {
10
+ readonly tenantUserIdsQuery?: string;
11
+ readonly rateLimit?: RateLimitConfig;
12
+ readonly isChannelKilled?: KillSwitchResolver;
13
+ };
14
+
15
+ /**
16
+ * Helper for setupTestStack: creates a DeliveryService + _notifyFactory for the test context.
17
+ * Abstracts the boilerplate that every Delivery-using test needs.
18
+ *
19
+ * Usage:
20
+ * setupTestStack({
21
+ * features: [...],
22
+ * extraContext: (deps) => createDeliveryTestContext(deps, { tenantUserIdsQuery: TenantQueries.resolveUserIds }),
23
+ * });
24
+ */
25
+ export function createDeliveryTestContext(
26
+ deps: { registry: Registry; db: DbConnection; sseBroker: SseBroker; redis: Redis },
27
+ options: CreateDeliveryTestContextOptions = {},
28
+ ): Record<string, unknown> & { deliveryService: DeliveryService } {
29
+ const { registry, db, sseBroker } = deps;
30
+ const channels = collectChannels(registry);
31
+ const deliveryService = createDeliveryService({
32
+ db,
33
+ registry,
34
+ sseBroker,
35
+ channels,
36
+ ...options,
37
+ });
38
+
39
+ return {
40
+ deliveryService, // exposed so tests can inspect/call directly if needed
41
+ _notifyFactory:
42
+ (user: { id: number; tenantId: TenantId }, tenantId: TenantId) =>
43
+ (notificationType: string, notifyOptions: Record<string, unknown>) =>
44
+ // @cast-boundary engine-bridge — generic test-helper → typed entity-specific notify()
45
+ deliveryService.notify(notificationType, notifyOptions as never, user as never, tenantId),
46
+ };
47
+ }
@@ -0,0 +1,71 @@
1
+ import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
+ import type {
4
+ NotifyOptions,
5
+ Registry,
6
+ SessionUser,
7
+ TenantId,
8
+ } from "@cosmicdrift/kumiko-framework/engine";
9
+
10
+ // --- Channel Interface ---
11
+
12
+ export type ChannelContext = {
13
+ readonly db: DbConnection;
14
+ readonly registry: Registry;
15
+ readonly sseBroker: SseBroker | undefined;
16
+ readonly tenantId: TenantId;
17
+ };
18
+
19
+ export type ChannelMessage = {
20
+ readonly notificationType: string;
21
+ readonly title: string;
22
+ readonly body: string | undefined;
23
+ readonly data: Readonly<Record<string, unknown>> | undefined;
24
+ };
25
+
26
+ export type ChannelResult = {
27
+ readonly status: "sent" | "failed" | "skipped";
28
+ readonly error?: string;
29
+ readonly address?: string;
30
+ };
31
+
32
+ export type DeliveryChannel = {
33
+ readonly name: string;
34
+ resolve(userId: string, ctx: ChannelContext): Promise<string | null>;
35
+ send(address: string, message: ChannelMessage, ctx: ChannelContext): Promise<ChannelResult>;
36
+ };
37
+
38
+ // --- Notification Renderer ---
39
+
40
+ export type RendererInput = {
41
+ readonly template: string;
42
+ readonly variables: Readonly<Record<string, unknown>>;
43
+ };
44
+
45
+ export type NotificationRenderer = {
46
+ readonly name: string;
47
+ render(input: RendererInput): Promise<string>;
48
+ };
49
+
50
+ // --- Delivery Log Entry ---
51
+
52
+ export type DeliveryLogEntry = {
53
+ readonly tenantId: TenantId;
54
+ readonly notificationType: string;
55
+ readonly channel: string;
56
+ readonly recipientId: string | null;
57
+ readonly recipientAddress: string | null;
58
+ readonly status: "sent" | "failed" | "skipped";
59
+ readonly error: string | null;
60
+ };
61
+
62
+ // --- Delivery Service ---
63
+
64
+ export type DeliveryService = {
65
+ notify(
66
+ notificationType: string,
67
+ options: NotifyOptions,
68
+ user: SessionUser,
69
+ tenantId: TenantId,
70
+ ): Promise<void>;
71
+ };
@@ -0,0 +1,99 @@
1
+ import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { Hono } from "hono";
4
+ import * as jose from "jose";
5
+ import { z } from "zod";
6
+ import { upsertPreference } from "./upsert-preference";
7
+
8
+ // Shape des verified-JWT-payloads. tenantId kommt als string aus jose und
9
+ // wird NACH erfolgreichem parse() zur Branded TenantId — kein blind-cast.
10
+ const unsubscribeJwtPayloadSchema = z.object({
11
+ sub: z.string().min(1),
12
+ tenantId: z.string().min(1),
13
+ notificationType: z.string().min(1),
14
+ channel: z.string().min(1),
15
+ });
16
+
17
+ export type UnsubscribeTokenPayload = {
18
+ readonly userId: string;
19
+ readonly tenantId: TenantId;
20
+ readonly notificationType: string;
21
+ readonly channel: string;
22
+ };
23
+
24
+ export type UnsubscribeRouteOptions = {
25
+ readonly db: DbConnection;
26
+ readonly jwtSecret: string;
27
+ };
28
+
29
+ const UNSUBSCRIBE_EXPIRY = "7d";
30
+ // The route runs outside the dispatcher — no SessionUser, no JWT middleware.
31
+ // Bill the event against the token-subject (the user owns their preference)
32
+ // and attribute it as a system-role action. This mirrors the way jobs and
33
+ // seeds attribute their out-of-band writes.
34
+ const SYSTEM_ROLES = ["system"] as const;
35
+
36
+ export async function signUnsubscribeToken(
37
+ payload: UnsubscribeTokenPayload,
38
+ secret: string,
39
+ ): Promise<string> {
40
+ const encodedSecret = new TextEncoder().encode(secret);
41
+ return new jose.SignJWT({
42
+ tenantId: payload.tenantId,
43
+ notificationType: payload.notificationType,
44
+ channel: payload.channel,
45
+ })
46
+ .setProtectedHeader({ alg: "HS256" })
47
+ .setSubject(String(payload.userId))
48
+ .setIssuer("kumiko:unsubscribe")
49
+ .setIssuedAt()
50
+ .setExpirationTime(UNSUBSCRIBE_EXPIRY)
51
+ .sign(encodedSecret);
52
+ }
53
+
54
+ export function createUnsubscribeRoute(options: UnsubscribeRouteOptions): Hono {
55
+ const { db, jwtSecret } = options;
56
+ const encodedSecret = new TextEncoder().encode(jwtSecret);
57
+ const app = new Hono();
58
+
59
+ app.get("/unsubscribe", async (c) => {
60
+ const token = c.req.query("token");
61
+ if (!token) {
62
+ return c.text("Missing token", 400);
63
+ }
64
+
65
+ let userId: string;
66
+ let tenantId: TenantId;
67
+ let notificationType: string;
68
+ let channel: string;
69
+ try {
70
+ const { payload } = await jose.jwtVerify(token, encodedSecret, {
71
+ issuer: "kumiko:unsubscribe",
72
+ });
73
+ const parsed = unsubscribeJwtPayloadSchema.parse(payload);
74
+ userId = parsed.sub;
75
+ // @cast-boundary engine-bridge — string post-zod → branded TenantId
76
+ tenantId = parsed.tenantId as TenantId;
77
+ notificationType = parsed.notificationType;
78
+ channel = parsed.channel;
79
+ } catch {
80
+ return c.text("Invalid or expired token", 400);
81
+ }
82
+
83
+ // Token-verify passed — everything below is a legitimate write. Don't
84
+ // swallow write-errors as "invalid token", that would mask real bugs
85
+ // (e.g. events-table missing, DB down) behind a misleading 400.
86
+ const actor = { id: userId, tenantId, roles: SYSTEM_ROLES };
87
+ const tdb = createTenantDb(db, tenantId, "system");
88
+ await upsertPreference(tdb, actor, {
89
+ tenantId,
90
+ userId,
91
+ notificationType,
92
+ channel,
93
+ enabled: false,
94
+ });
95
+ return c.text("You have been unsubscribed.", 200);
96
+ });
97
+
98
+ return app;
99
+ }
@@ -0,0 +1,145 @@
1
+ // Race-safe upsert for notification-preferences. Pre-ES this was a single
2
+ // `onConflictDoUpdate` statement on the preferences table; post-ES we go
3
+ // through the event-store executor, which doesn't offer a built-in upsert.
4
+ // Splitting into lookup + create|update re-opens the race window for
5
+ // concurrent requests — typical case: a user clicks the same "unsubscribe"
6
+ // email link three times in a second.
7
+ //
8
+ // The fix: try the optimistic path (lookup + create|update), and on a
9
+ // unique-index-violation race from a parallel create, re-lookup and fall
10
+ // through to update. Worst case: one extra roundtrip for the loser of
11
+ // the race. Happy path: same number of queries as the pre-ES upsert.
12
+
13
+ import {
14
+ createEventStoreExecutor,
15
+ fetchOne,
16
+ type TenantDb,
17
+ } from "@cosmicdrift/kumiko-framework/db";
18
+ import type { SessionUser, TenantId, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
19
+ import { eq } from "drizzle-orm";
20
+ import { notificationPreferenceEntity, notificationPreferencesTable } from "./tables";
21
+
22
+ const executor = createEventStoreExecutor(
23
+ notificationPreferencesTable,
24
+ notificationPreferenceEntity,
25
+ { entityName: "notification-preference" },
26
+ );
27
+
28
+ // Konkretes Lookup-Result-Shape — nur die zwei Felder die der upsert
29
+ // tatsächlich für den Update-Path braucht. Vermeidet `row["x"] as T` index-
30
+ // access casts; der Generic-Param am fetchOne macht den Cast zentralisiert
31
+ // im Helper (db-row-boundary), nicht 4× pro Callsite.
32
+ type PreferenceLookupRow = { readonly id: string; readonly version: number };
33
+
34
+ async function lookup(
35
+ db: TenantDb,
36
+ tenantId: TenantId,
37
+ userId: string,
38
+ notificationType: string,
39
+ channel: string,
40
+ ): Promise<PreferenceLookupRow | undefined> {
41
+ return fetchOne<PreferenceLookupRow>(
42
+ db,
43
+ notificationPreferencesTable,
44
+ eq(notificationPreferencesTable.tenantId, tenantId),
45
+ eq(notificationPreferencesTable.userId, userId),
46
+ eq(notificationPreferencesTable.notificationType, notificationType),
47
+ eq(notificationPreferencesTable.channel, channel),
48
+ );
49
+ }
50
+
51
+ export type UpsertPreferenceInput = {
52
+ readonly tenantId: TenantId;
53
+ readonly userId: string;
54
+ readonly notificationType: string;
55
+ readonly channel: string;
56
+ readonly enabled: boolean;
57
+ };
58
+
59
+ /**
60
+ * Idempotent "set-this-preference-to-enabled-state" against the preferences
61
+ * aggregate stream. Emits either `.created` (first time) or `.updated`
62
+ * (subsequent) and catches the race-induced unique-index violation as a
63
+ * fallback to update.
64
+ */
65
+ export async function upsertPreference(
66
+ db: TenantDb,
67
+ actor: SessionUser,
68
+ input: UpsertPreferenceInput,
69
+ ): Promise<WriteResult<UpsertPreferenceInput>> {
70
+ const existing = await lookup(
71
+ db,
72
+ input.tenantId,
73
+ input.userId,
74
+ input.notificationType,
75
+ input.channel,
76
+ );
77
+
78
+ if (existing) {
79
+ const result = await executor.update(
80
+ {
81
+ id: existing.id,
82
+ version: existing.version,
83
+ changes: { enabled: input.enabled },
84
+ },
85
+ actor,
86
+ db,
87
+ );
88
+ if (!result.isSuccess) return result;
89
+ return { isSuccess: true, data: input };
90
+ }
91
+
92
+ try {
93
+ const result = await executor.create(
94
+ {
95
+ userId: input.userId,
96
+ notificationType: input.notificationType,
97
+ channel: input.channel,
98
+ enabled: input.enabled,
99
+ },
100
+ actor,
101
+ db,
102
+ );
103
+ if (!result.isSuccess) return result;
104
+ return { isSuccess: true, data: input };
105
+ } catch (err) {
106
+ // Race-fallback: another request beat us to the insert between our
107
+ // lookup and the executor.create. The unique-index on
108
+ // (tenant, user, type, channel) fires Postgres error 23505. Only that
109
+ // specific error triggers the retry; DB-disconnect or any other
110
+ // failure must bubble up unchanged so callers see the real cause.
111
+ if (!isUniqueViolation(err)) throw err;
112
+ const afterRace = await lookup(
113
+ db,
114
+ input.tenantId,
115
+ input.userId,
116
+ input.notificationType,
117
+ input.channel,
118
+ );
119
+ if (!afterRace) throw err;
120
+ const result = await executor.update(
121
+ {
122
+ id: afterRace.id,
123
+ version: afterRace.version,
124
+ changes: { enabled: input.enabled },
125
+ },
126
+ actor,
127
+ db,
128
+ );
129
+ if (!result.isSuccess) return result;
130
+ return { isSuccess: true, data: input };
131
+ }
132
+ }
133
+
134
+ // Narrow detection for Postgres "duplicate key violates unique constraint"
135
+ // (SQLSTATE 23505). Drivers wrap the DB error in varying envelopes; we
136
+ // check the `code` field plus a string-match fallback so the match survives
137
+ // minor driver-version shifts without drifting wide.
138
+ function isUniqueViolation(err: unknown): boolean {
139
+ if (typeof err !== "object" || err === null) return false;
140
+ const e = err as { code?: unknown; cause?: { code?: unknown }; message?: unknown };
141
+ if (e.code === "23505") return true;
142
+ if (e.cause && typeof e.cause === "object" && e.cause.code === "23505") return true;
143
+ if (typeof e.message === "string" && e.message.includes("23505")) return true;
144
+ return false;
145
+ }