@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,192 @@
1
+ // Tenant-Invite Step 2 — Branch 3 (anon User, Email NICHT registriert).
2
+ //
3
+ // Anders als signup-confirm: KEIN neuer Tenant entsteht. Der Tenant
4
+ // existiert schon (im invitation-row), wir legen NUR User+Membership an.
5
+ //
6
+ // Flow:
7
+ // 1. User klickt Invite-Link → /invite/signup?token=...
8
+ // 2. Frontend zeigt Password-Form (email kommt aus invitation, kein
9
+ // User-Input damit kein Email-Mismatch möglich)
10
+ // 3. User submitted password + token
11
+ // 4. Server:
12
+ // a. Token → invitationId → invitation row
13
+ // b. User-Existence-Check: invitation.email darf NICHT in userTable
14
+ // existieren (sonst soll Branch 2 oder Branch 1 genutzt werden)
15
+ // c. Create user (emailVerified=true wegen Magic-Link)
16
+ // d. Add membership im invited Tenant
17
+ // e. Invitation → accepted, Token gelöscht
18
+ // 5. Response: SessionUser + tenantId für Auto-Login
19
+
20
+ import {
21
+ createEventStoreExecutor,
22
+ createTenantDb,
23
+ type DbConnection,
24
+ fetchOne,
25
+ } from "@cosmicdrift/kumiko-framework/db";
26
+ import {
27
+ createSystemUser,
28
+ defineWriteHandler,
29
+ type SessionUser,
30
+ type TenantId,
31
+ } from "@cosmicdrift/kumiko-framework/engine";
32
+ import {
33
+ InternalError,
34
+ UnprocessableError,
35
+ writeFailure,
36
+ } from "@cosmicdrift/kumiko-framework/errors";
37
+ import { eq } from "drizzle-orm";
38
+ import { z } from "zod";
39
+ // kumiko-lint-ignore cross-feature-import invite-flow
40
+ import {
41
+ INVITATION_STATUS,
42
+ tenantInvitationEntity,
43
+ tenantInvitationsTable,
44
+ } from "../../tenant/invitation-table";
45
+ // kumiko-lint-ignore cross-feature-import membership-seed-helper für privilegierten cross-tenant-add
46
+ import { seedTenantMembership } from "../../tenant/seeding";
47
+ // kumiko-lint-ignore cross-feature-import existence-check
48
+ import { userTable } from "../../user/schema/user";
49
+ import { AuthErrors } from "../constants";
50
+ import {
51
+ burnInviteToken,
52
+ deleteInviteToken,
53
+ getInvitationIdForToken,
54
+ unburnInviteToken,
55
+ } from "../invite-token-store";
56
+ // kumiko-lint-ignore cross-feature-import provisioning needs cross-feature seeding helpers
57
+ import { seedUserWithPassword } from "../seeding";
58
+
59
+ const InviteSignupCompleteSchema = z.object({
60
+ token: z.string().min(1),
61
+ password: z.string().min(8).max(200),
62
+ });
63
+
64
+ export type InviteSignupCompleteData = {
65
+ readonly kind: "auth-session";
66
+ readonly session: SessionUser;
67
+ readonly tenantId: TenantId;
68
+ readonly role: string;
69
+ };
70
+
71
+ const invitationExecutor = createEventStoreExecutor(
72
+ tenantInvitationsTable,
73
+ tenantInvitationEntity,
74
+ { entityName: "tenant-invitation" },
75
+ );
76
+
77
+ function invalidInviteToken() {
78
+ return writeFailure(
79
+ new UnprocessableError(AuthErrors.invalidInviteToken, {
80
+ i18nKey: "auth.errors.invalidInviteToken",
81
+ }),
82
+ );
83
+ }
84
+
85
+ export function createInviteSignupCompleteHandler() {
86
+ return defineWriteHandler<
87
+ "invite-signup-complete",
88
+ typeof InviteSignupCompleteSchema,
89
+ InviteSignupCompleteData
90
+ >({
91
+ name: "invite-signup-complete",
92
+ schema: InviteSignupCompleteSchema,
93
+ access: { roles: ["all"] },
94
+ handler: async (event, ctx) => {
95
+ if (!ctx.redis) {
96
+ return writeFailure(
97
+ new InternalError({ message: "invite-signup-complete requires ctx.redis" }),
98
+ );
99
+ }
100
+
101
+ const invitationId = await getInvitationIdForToken(ctx.redis, event.payload.token);
102
+ if (!invitationId) return invalidInviteToken();
103
+
104
+ const burn = await burnInviteToken(ctx.redis, event.payload.token);
105
+ if (burn === "already-used") return invalidInviteToken();
106
+
107
+ let committed = false;
108
+ try {
109
+ const invitation = await fetchOne(
110
+ ctx.db.raw,
111
+ tenantInvitationsTable,
112
+ eq(tenantInvitationsTable.id, invitationId),
113
+ );
114
+ if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
115
+ return invalidInviteToken();
116
+
117
+ const invitationTenantId = invitation["tenantId"] as TenantId;
118
+ const invitationEmail = invitation["email"] as string;
119
+ const invitationRole = invitation["role"] as string;
120
+ const invitationVersion = invitation["version"] as number;
121
+
122
+ // User-Not-Exists-Check: wenn die Email schon registriert ist,
123
+ // muss der User Branch 2 (acceptWithLogin) nutzen. Hier ist
124
+ // explizit "neue Email" — sonst hätten wir zwei Wege ein
125
+ // Password zu setzen für denselben User.
126
+ const existingUser = await fetchOne(
127
+ ctx.db.raw,
128
+ userTable,
129
+ eq(userTable.email, invitationEmail),
130
+ );
131
+ if (existingUser) return invalidInviteToken();
132
+
133
+ // User anlegen via seedUserWithPassword (gleiches Pattern wie
134
+ // signup-confirm), emailVerified=true wegen Magic-Link.
135
+ // @cast-boundary db-runner — TenantDb.raw is DbRunner; seed-helpers
136
+ // operate on plain drizzle-API which both shapes expose identically.
137
+ const dbConn = ctx.db.raw as DbConnection;
138
+ const userId = await seedUserWithPassword(dbConn, {
139
+ email: invitationEmail,
140
+ password: event.payload.password,
141
+ displayName: invitationEmail.split("@")[0] ?? invitationEmail,
142
+ emailVerified: true,
143
+ });
144
+
145
+ // Membership-Add via seed-helper (gleiches Pattern wie
146
+ // provisionSignupAccount — bypassed addMember access-check
147
+ // weil createSystemUser nicht ["SystemAdmin"] matcht).
148
+ await seedTenantMembership(dbConn, {
149
+ userId,
150
+ tenantId: invitationTenantId,
151
+ roles: [invitationRole],
152
+ });
153
+
154
+ // Invitation → accepted: TenantDb für invitation-tenant
155
+ const invitationTdb = createTenantDb(dbConn, invitationTenantId, "system");
156
+ const updateResult = await invitationExecutor.update(
157
+ {
158
+ id: invitationId,
159
+ version: invitationVersion,
160
+ changes: { status: INVITATION_STATUS.accepted },
161
+ },
162
+ createSystemUser(invitationTenantId),
163
+ invitationTdb,
164
+ );
165
+ if (!updateResult.isSuccess) return updateResult;
166
+
167
+ await deleteInviteToken(ctx.redis, { invitationId, token: event.payload.token });
168
+
169
+ const session: SessionUser = {
170
+ id: userId,
171
+ tenantId: invitationTenantId,
172
+ roles: [invitationRole],
173
+ };
174
+
175
+ committed = true;
176
+ return {
177
+ isSuccess: true,
178
+ data: {
179
+ kind: "auth-session",
180
+ session,
181
+ tenantId: invitationTenantId,
182
+ role: invitationRole,
183
+ },
184
+ };
185
+ } finally {
186
+ if (!committed && ctx.redis) {
187
+ await unburnInviteToken(ctx.redis, event.payload.token);
188
+ }
189
+ }
190
+ },
191
+ });
192
+ }
@@ -0,0 +1,208 @@
1
+ import {
2
+ createSystemUser,
3
+ defineWriteHandler,
4
+ type SessionUser,
5
+ type TenantId,
6
+ } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
8
+ import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
9
+ import { z } from "zod";
10
+ import { UserQueries } from "../../user";
11
+ import { parseAuthUserRow } from "../auth-user-row";
12
+ import {
13
+ AUTH_LOCKOUT_DEFAULT_DURATION_MINUTES,
14
+ AUTH_LOCKOUT_DEFAULT_MAX_FAILED_ATTEMPTS,
15
+ AuthErrors,
16
+ } from "../constants";
17
+ import { clearLockoutState, getLockoutState, recordFailedAttempt } from "../lockout-store";
18
+ import { verifyPassword } from "../password-hashing";
19
+
20
+ function invalidCredentials() {
21
+ return writeFailure(
22
+ new UnprocessableError(AuthErrors.invalidCredentials, {
23
+ i18nKey: "auth.errors.invalidCredentials",
24
+ }),
25
+ );
26
+ }
27
+
28
+ function noMembership() {
29
+ return writeFailure(
30
+ new UnprocessableError(AuthErrors.noMembership, {
31
+ i18nKey: "auth.errors.noMembership",
32
+ }),
33
+ );
34
+ }
35
+
36
+ function emailNotVerified() {
37
+ return writeFailure(
38
+ new UnprocessableError(AuthErrors.emailNotVerified, {
39
+ i18nKey: "auth.errors.emailNotVerified",
40
+ }),
41
+ );
42
+ }
43
+
44
+ function accountLocked(retryAfterSeconds: number) {
45
+ return writeFailure(
46
+ new UnprocessableError(AuthErrors.accountLocked, {
47
+ i18nKey: "auth.errors.accountLocked",
48
+ // Seconds until auto-unlock — UI renders a countdown, clients can
49
+ // schedule a retry. Rounded up so the UI never shows 0 while the
50
+ // lock is still active.
51
+ details: { retryAfterSeconds },
52
+ }),
53
+ );
54
+ }
55
+
56
+ export type LoginHandlerOptions = {
57
+ // When true, a valid (email + password) login fails with email_not_verified
58
+ // if the user row's emailVerified flag is false. Enumeration-leak is
59
+ // accepted: UX benefit ("check your email") outweighs the marginal
60
+ // signal since signup already surfaces the same fact.
61
+ readonly strictEmailVerification?: boolean;
62
+ // Brute-force protection: after N wrong-password attempts the account
63
+ // locks for the configured duration. State lives in Redis (see
64
+ // lockout-store.ts) — if ctx.redis is unset, lockout is skipped and the
65
+ // handler falls back to classic invalid-credentials. Counter is monotonic
66
+ // and only resets on a successful login, so a re-lock after the cooldown
67
+ // happens on the FIRST miss, not the Nth (strict semantic — favours
68
+ // brute-force resistance over UX).
69
+ readonly accountLockout?: {
70
+ readonly maxFailedAttempts?: number;
71
+ readonly lockoutDurationMinutes?: number;
72
+ };
73
+ };
74
+
75
+ const SYSTEM_USER_ID = "00000000-0000-4000-8000-000000000000";
76
+
77
+ // Login — unauthenticated entry point. The route is wired public (no JWT
78
+ // middleware), synthesising a guest SessionUser for the handler's access
79
+ // check. Everything inside the handler goes through ctx.queryAs(system, ...)
80
+ // so the user feature stays the single owner of its table.
81
+ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
82
+ const strictVerification = opts.strictEmailVerification === true;
83
+ const maxFailedAttempts =
84
+ opts.accountLockout?.maxFailedAttempts ?? AUTH_LOCKOUT_DEFAULT_MAX_FAILED_ATTEMPTS;
85
+ const lockoutDurationMinutes =
86
+ opts.accountLockout?.lockoutDurationMinutes ?? AUTH_LOCKOUT_DEFAULT_DURATION_MINUTES;
87
+
88
+ return defineWriteHandler({
89
+ name: "login",
90
+ schema: z.object({
91
+ email: z.email(),
92
+ password: z.string().min(1),
93
+ }),
94
+ access: { roles: ["all"] },
95
+ handler: async (event, ctx) => {
96
+ const systemUser = createSystemUser(SYSTEM_USER_ID);
97
+
98
+ const found = parseAuthUserRow(
99
+ await ctx.queryAs(systemUser, UserQueries.findForAuth, {
100
+ email: event.payload.email,
101
+ }),
102
+ );
103
+
104
+ // Uniform response on any credential mismatch (no user, wrong password,
105
+ // soft-deleted user) — prevents email enumeration.
106
+ if (!found?.passwordHash || found.isDeleted) {
107
+ return invalidCredentials();
108
+ }
109
+
110
+ // Lockout gate — runs BEFORE password verification so a locked account
111
+ // can't be bruteforce-probed for passwords (and also can't be probed
112
+ // for a timing-oracle on the bcrypt verify). If Redis isn't wired,
113
+ // lockout is silently skipped — login still works, brute-force
114
+ // protection just degrades to the IP-rate-limiter at the edge.
115
+ if (ctx.redis) {
116
+ const state = await getLockoutState(ctx.redis, found.id);
117
+ if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
118
+ const now = Date.now();
119
+ if (state.lockedUntil > now) {
120
+ const retryAfterSeconds = Math.max(1, Math.ceil((state.lockedUntil - now) / 1000));
121
+ return accountLocked(retryAfterSeconds);
122
+ }
123
+ // lockedUntil in the past — shouldn't normally happen because the
124
+ // Redis TTL on the until-key expires the key at the same moment
125
+ // as the value. Clock skew / replication lag could surface this;
126
+ // fall through to password verification. The counter is NOT
127
+ // reset — next miss re-locks immediately (strict-semantic, see
128
+ // lockout-store.ts).
129
+ }
130
+ }
131
+
132
+ const passwordOk = await verifyPassword(found.passwordHash, event.payload.password);
133
+ if (!passwordOk) {
134
+ if (ctx.redis) {
135
+ await recordFailedAttempt(ctx.redis, found.id, maxFailedAttempts, lockoutDurationMinutes);
136
+ }
137
+ return invalidCredentials();
138
+ }
139
+
140
+ // Strict verification gate — runs AFTER password check so an attacker
141
+ // probing "email_not_verified" needs valid credentials first. The
142
+ // remaining enumeration surface is "valid-cred + unverified" → accepted
143
+ // leak because the signup flow already told the user "check your email".
144
+ if (strictVerification && found.emailVerified !== true) {
145
+ return emailNotVerified();
146
+ }
147
+
148
+ // Resolve tenant + roles via the tenant feature's memberships query.
149
+ // Returns [] if the user has no memberships — MVP: no login without an
150
+ // invitation, so we refuse with a dedicated error.
151
+ const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
152
+ userId: found.id,
153
+ })) as Array<{ tenantId: TenantId; roles: readonly string[] }>;
154
+
155
+ if (memberships.length === 0) {
156
+ return noMembership();
157
+ }
158
+
159
+ const preferred =
160
+ found.lastActiveTenantId !== null && found.lastActiveTenantId !== undefined
161
+ ? memberships.find((m) => m.tenantId === found.lastActiveTenantId)
162
+ : undefined;
163
+ const chosen = preferred ?? memberships[0];
164
+ if (!chosen) {
165
+ return noMembership();
166
+ }
167
+
168
+ // Clear the lockout state on success. DEL is idempotent, so no need
169
+ // to gate on "was there a counter?" — skipping the Redis round-trip
170
+ // entirely for users who never failed a login would optimise the hot
171
+ // path, but the call is microseconds and the branch isn't free either.
172
+ if (ctx.redis) {
173
+ await clearLockoutState(ctx.redis, found.id);
174
+ }
175
+
176
+ // Globale Rollen aus user.roles + tenant-membership-roles mergen.
177
+ // Globale Rollen (SystemAdmin etc.) bleiben so über alle tenants
178
+ // gleich; tenant-spezifische Rollen (Admin, User) kommen aus der
179
+ // membership. Dedupe via Set damit eine Rolle die in beiden Quellen
180
+ // steht nicht doppelt im Session-Roles landet.
181
+ const globalRoles = parseRoles(found.roles ?? null);
182
+ const mergedRoles = Array.from(new Set([...globalRoles, ...chosen.roles]));
183
+ const baseSession: SessionUser = {
184
+ id: found.id,
185
+ tenantId: chosen.tenantId,
186
+ roles: mergedRoles,
187
+ };
188
+
189
+ // Features can contribute identity facts (team IDs, feature flags, ...)
190
+ // via r.authClaims(). ctx.resolveAuthClaims is a thin pass-through to
191
+ // dispatcher.resolveAuthClaims — same impl also used by the switch-tenant
192
+ // route, so login + tenant-switch stay in sync.
193
+ //
194
+ // Best-effort: if no feature registered a hook, we get an empty record
195
+ // back and simply omit the `claims` field from the session (keeps the
196
+ // shape clean for the JWT layer, which already spreads claims
197
+ // conditionally based on presence).
198
+ const claims = await ctx.resolveAuthClaims(baseSession);
199
+ const session: SessionUser =
200
+ Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
201
+
202
+ return {
203
+ isSuccess: true,
204
+ data: { kind: "auth-session", session },
205
+ };
206
+ },
207
+ });
208
+ }
@@ -0,0 +1,12 @@
1
+ import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { z } from "zod";
3
+
4
+ // Logout — JWT is stateless, so server-side we only return OK. A future
5
+ // revocation list / session table can land here without changing the
6
+ // route or client API. Keeping the handler makes the API shape stable.
7
+ export const logoutWrite = defineWriteHandler({
8
+ name: "logout",
9
+ schema: z.object({}),
10
+ access: { roles: access.authenticated },
11
+ handler: async () => ({ isSuccess: true, data: { kind: "logged-out" } }),
12
+ });
@@ -0,0 +1,29 @@
1
+ import { AUTH_VERIFY_DEFAULT_TTL_MINUTES, AuthErrors } from "../constants";
2
+ import { signVerificationToken } from "../verification-token";
3
+ import {
4
+ createTokenRequestHandler,
5
+ type TokenRequestData,
6
+ type TokenRequestOptions,
7
+ } from "./token-request-handler";
8
+
9
+ export type RequestEmailVerificationOptions = TokenRequestOptions;
10
+
11
+ export type RequestVerificationData = TokenRequestData<"verification-requested">;
12
+
13
+ export function createRequestEmailVerificationHandler(opts: RequestEmailVerificationOptions) {
14
+ return createTokenRequestHandler(
15
+ {
16
+ handlerName: "request-email-verification",
17
+ successKind: "verification-requested",
18
+ defaultTtlMinutes: AUTH_VERIFY_DEFAULT_TTL_MINUTES,
19
+ sign: signVerificationToken,
20
+ notConfiguredError: AuthErrors.verificationNotConfigured,
21
+ notConfiguredI18nKey: "auth.errors.verificationNotConfigured",
22
+ // Silent no-op for already-verified users. Flipped-together with
23
+ // unknown/deleted to keep the enumeration surface symmetric — the
24
+ // caller sees the same 200 regardless of whether a token was minted.
25
+ extraSilentSkip: (user) => user.emailVerified === true,
26
+ },
27
+ opts,
28
+ );
29
+ }
@@ -0,0 +1,31 @@
1
+ import { AUTH_RESET_DEFAULT_TTL_MINUTES, AuthErrors } from "../constants";
2
+ import { signResetToken } from "../reset-token";
3
+ import {
4
+ createTokenRequestHandler,
5
+ type TokenRequestData,
6
+ type TokenRequestOptions,
7
+ } from "./token-request-handler";
8
+
9
+ export type RequestPasswordResetOptions = TokenRequestOptions;
10
+
11
+ // Public shape re-exported for callers that build custom routes on top of
12
+ // the dispatcher (bypassing the framework's auth-routes).
13
+ export type RequestResetData = TokenRequestData<"reset-requested">;
14
+
15
+ export function createRequestPasswordResetHandler(opts: RequestPasswordResetOptions) {
16
+ return createTokenRequestHandler(
17
+ {
18
+ handlerName: "request-password-reset",
19
+ successKind: "reset-requested",
20
+ defaultTtlMinutes: AUTH_RESET_DEFAULT_TTL_MINUTES,
21
+ sign: signResetToken,
22
+ notConfiguredError: AuthErrors.resetNotConfigured,
23
+ notConfiguredI18nKey: "auth.errors.resetNotConfigured",
24
+ // Password-reset has no extra skip condition — every existing,
25
+ // non-deleted user can initiate a reset regardless of verification
26
+ // state. The sessions feature handles the post-change revocation.
27
+ extraSilentSkip: () => false,
28
+ },
29
+ opts,
30
+ );
31
+ }
@@ -0,0 +1,61 @@
1
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { z } from "zod";
4
+ import { AuthErrors } from "../constants";
5
+ import { hashPassword } from "../password-hashing";
6
+ import { verifyResetToken } from "../reset-token";
7
+ import { runConfirmTokenFlow } from "./confirm-token-flow";
8
+
9
+ export type ResetPasswordOptions = {
10
+ readonly hmacSecret: string;
11
+ };
12
+
13
+ function invalidToken() {
14
+ return writeFailure(
15
+ new UnprocessableError(AuthErrors.invalidResetToken, {
16
+ i18nKey: "auth.errors.invalidResetToken",
17
+ }),
18
+ );
19
+ }
20
+
21
+ // Confirm step of the reset flow. Token-verify happens inline; the
22
+ // post-verify pipeline (burn, load user, memberships, try-all-tenants,
23
+ // burn-release-on-failure) lives in confirm-token-flow to stay in sync
24
+ // with verify-email. Session revocation on password change is wired
25
+ // cross-feature via the sessions feature's r.entityHook("postSave",
26
+ // "user", ...) — no explicit revoke call needed here.
27
+ export function createResetPasswordHandler(opts: ResetPasswordOptions) {
28
+ return defineWriteHandler({
29
+ name: "reset-password",
30
+ schema: z.object({
31
+ token: z.string().min(1),
32
+ newPassword: z.string().min(8).max(200),
33
+ }),
34
+ access: { roles: ["all"] },
35
+ handler: async (event, ctx) => {
36
+ if (!opts.hmacSecret) {
37
+ return writeFailure(
38
+ new UnprocessableError(AuthErrors.resetNotConfigured, {
39
+ i18nKey: "auth.errors.resetNotConfigured",
40
+ }),
41
+ );
42
+ }
43
+
44
+ // All verify failures (malformed / bad_signature / expired) fold into
45
+ // the same invalid_reset_token error — a probing caller can't
46
+ // distinguish tampered from stale from random-string.
47
+ const verify = verifyResetToken(event.payload.token, opts.hmacSecret);
48
+ if (!verify.ok) return invalidToken();
49
+
50
+ return runConfirmTokenFlow(ctx, verify.userId, verify.expiresAtMs, {
51
+ purpose: "reset",
52
+ redisRequiredMessage: "password-reset requires ctx.redis to enforce token single-use",
53
+ invalidToken,
54
+ buildChanges: async () => ({
55
+ passwordHash: await hashPassword(event.payload.newPassword),
56
+ }),
57
+ successData: { kind: "password-reset" as const },
58
+ });
59
+ },
60
+ });
61
+ }