@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,101 @@
1
+ // Tests the `sessionStrictMode` flag on AuthRoutesConfig. When enabled, a
2
+ // JWT that arrives WITHOUT a `jti` is rejected at the middleware — useful
3
+ // after a rolling deploy has been emitting sids longer than the JWT TTL,
4
+ // so legacy stateless tokens are expected to have expired. Default false
5
+ // keeps pre-upgrade tokens working; this suite flips it on and asserts.
6
+
7
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
8
+ import {
9
+ setupTestStack,
10
+ type TestStack,
11
+ TestUsers,
12
+ testTenantId,
13
+ } from "@cosmicdrift/kumiko-framework/stack";
14
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
15
+ import { createConfigFeature } from "../../config";
16
+ import { createTenantFeature } from "../../tenant";
17
+ import { createUserFeature } from "../../user";
18
+ import { createAuthEmailPasswordFeature } from "../feature";
19
+
20
+ let stack: TestStack;
21
+ const TENANT: TenantId = testTenantId(1);
22
+ const userId = TestUsers.systemAdmin.id;
23
+
24
+ // Stub checker that always accepts. The strictMode branch runs BEFORE the
25
+ // checker is even consulted (no jti → nothing to check), so the stub never
26
+ // fires in the strict-mode path. It's present to satisfy the framework's
27
+ // "you wired sessionChecker, so we'll run it if we have an sid" contract.
28
+ // Accepts the full AuthSessionChecker signature (sid + expectedUserId)
29
+ // even though it doesn't use the args.
30
+ async function stubChecker(_sid: string, _expectedUserId: string): Promise<"live"> {
31
+ return "live";
32
+ }
33
+
34
+ beforeAll(async () => {
35
+ stack = await setupTestStack({
36
+ features: [
37
+ createConfigFeature(),
38
+ createUserFeature(),
39
+ createTenantFeature(),
40
+ createAuthEmailPasswordFeature(),
41
+ ],
42
+ authConfig: {
43
+ membershipQuery: "tenant:query:memberships",
44
+ sessionChecker: stubChecker,
45
+ sessionStrictMode: true,
46
+ },
47
+ });
48
+ });
49
+
50
+ afterAll(async () => {
51
+ await stack.cleanup();
52
+ });
53
+
54
+ describe("sessionStrictMode: sidless JWTs are rejected", () => {
55
+ test("JWT without jti → 401 with reason=no_sid", async () => {
56
+ // Hand-signed JWT that carries id + tenantId + roles but NO jti. The
57
+ // standard testing request-helper signs JWTs the same way on user
58
+ // arguments without a sid field.
59
+ const token = await stack.jwt.sign({ id: userId, tenantId: TENANT, roles: ["SystemAdmin"] });
60
+
61
+ const res = await stack.http.raw(
62
+ "POST",
63
+ "/api/query",
64
+ { type: "user:query:user:me", payload: {} },
65
+ { Authorization: `Bearer ${token}` },
66
+ );
67
+ expect(res.status).toBe(401);
68
+ const body = (await res.json()) as { error?: { details?: { reason?: string } } };
69
+ expect(body.error?.details?.reason).toBe("no_sid");
70
+ });
71
+
72
+ test("JWT WITH jti passes the middleware gate (stubChecker returns 'live')", async () => {
73
+ const token = await stack.jwt.sign({
74
+ id: userId,
75
+ tenantId: TENANT,
76
+ roles: ["SystemAdmin"],
77
+ sid: "aaaa1111-bbbb-2222-cccc-3333dddd4444",
78
+ });
79
+
80
+ // Hit /health — it's in PUBLIC_API_PATHS and bypasses auth entirely,
81
+ // so a success there tells us nothing. Instead send to a known handler
82
+ // and just assert the middleware didn't turn it into a 401. The
83
+ // minimal stack has no user-table; the me-query would 500 on its SQL
84
+ // call, which is fine for this test (we're specifically NOT making
85
+ // statements about the handler behaviour). 401 means "middleware
86
+ // blocked us", which is exactly the bug this suite catches.
87
+ const res = await stack.http.raw(
88
+ "POST",
89
+ "/api/query",
90
+ { type: "user:query:user:me", payload: {} },
91
+ { Authorization: `Bearer ${token}` },
92
+ );
93
+ expect(res.status).not.toBe(401);
94
+ // Narrow the "not 401" to exclude other 4xx middleware errors too.
95
+ // A 403 from access-layer or a 400 from shape-validation wouldn't
96
+ // come from sessionStrictMode, but either would be a different code
97
+ // path than the one we're testing — flag them if they surface.
98
+ expect(res.status).not.toBe(403);
99
+ expect(res.status).not.toBe(400);
100
+ });
101
+ });
@@ -0,0 +1,78 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { describe, expect, test } from "vitest";
3
+ import { signToken, TokenPurpose, verifyToken } from "../signed-token";
4
+
5
+ const SECRET = "test-hmac-secret-32-bytes-minimum!!";
6
+ const USER_ID = "11111111-1111-4111-8111-111111111111";
7
+
8
+ describe("signed-token", () => {
9
+ test("round-trip: sign → verify (matching purpose) → userId", () => {
10
+ const { token } = signToken(USER_ID, TokenPurpose.passwordReset, 15, SECRET);
11
+ const result = verifyToken(token, TokenPurpose.passwordReset, SECRET);
12
+ expect(result.ok).toBe(true);
13
+ if (result.ok) expect(result.userId).toBe(USER_ID);
14
+ });
15
+
16
+ test("cross-purpose replay is rejected (reset token on verify endpoint)", () => {
17
+ const { token } = signToken(USER_ID, TokenPurpose.passwordReset, 15, SECRET);
18
+ const result = verifyToken(token, TokenPurpose.emailVerification, SECRET);
19
+ expect(result).toEqual({ ok: false, reason: "bad_signature" });
20
+ });
21
+
22
+ test("tampered signature → bad_signature", () => {
23
+ const { token } = signToken(USER_ID, TokenPurpose.passwordReset, 15, SECRET);
24
+ const tampered = `${token.slice(0, -3)}XXX`;
25
+ const result = verifyToken(tampered, TokenPurpose.passwordReset, SECRET);
26
+ expect(result).toEqual({ ok: false, reason: "bad_signature" });
27
+ });
28
+
29
+ test("different secret → bad_signature", () => {
30
+ const { token } = signToken(USER_ID, TokenPurpose.passwordReset, 15, SECRET);
31
+ const result = verifyToken(
32
+ token,
33
+ TokenPurpose.passwordReset,
34
+ "other-secret-not-the-same-one!!!!",
35
+ );
36
+ expect(result).toEqual({ ok: false, reason: "bad_signature" });
37
+ });
38
+
39
+ test("expired → expired", () => {
40
+ const t0 = Temporal.Instant.fromEpochMilliseconds(1_700_000_000_000);
41
+ const laterThanTtl = t0.add({ minutes: 16 });
42
+ const { token } = signToken(USER_ID, TokenPurpose.passwordReset, 15, SECRET, t0);
43
+ const result = verifyToken(token, TokenPurpose.passwordReset, SECRET, laterThanTtl);
44
+ expect(result).toEqual({ ok: false, reason: "expired" });
45
+ });
46
+
47
+ test("malformed: wrong part count", () => {
48
+ expect(verifyToken("not-a-token", "reset", SECRET)).toEqual({
49
+ ok: false,
50
+ reason: "malformed",
51
+ });
52
+ expect(verifyToken("a.b", "reset", SECRET)).toEqual({
53
+ ok: false,
54
+ reason: "malformed",
55
+ });
56
+ expect(verifyToken("a.b.c.d", "reset", SECRET)).toEqual({
57
+ ok: false,
58
+ reason: "malformed",
59
+ });
60
+ });
61
+
62
+ test("malformed: non-numeric expiry", () => {
63
+ const result = verifyToken(`${USER_ID}.not-a-number.sig`, "reset", SECRET);
64
+ expect(result).toEqual({ ok: false, reason: "malformed" });
65
+ });
66
+
67
+ test("empty parts count as malformed", () => {
68
+ const result = verifyToken("..", "reset", SECRET);
69
+ expect(result).toEqual({ ok: false, reason: "malformed" });
70
+ });
71
+
72
+ test("expiresAt reflects configured TTL", () => {
73
+ const t0 = Temporal.Instant.fromEpochMilliseconds(1_700_000_000_000);
74
+ const { expiresAt } = signToken(USER_ID, "reset", 30, SECRET, t0);
75
+ const diff = expiresAt.since(t0).total({ unit: "minutes" });
76
+ expect(diff).toBe(30);
77
+ });
78
+ });
@@ -0,0 +1,259 @@
1
+ // Magic-Link-Self-Signup Full-Stack Integration-Test. Spec ist der
2
+ // Test selbst (advisor-Empfehlung). Geht durch HTTP, weil
3
+ // stack.dispatcher nicht exposed ist und die Routes ohnehin der
4
+ // reale User-Pfad sind.
5
+ //
6
+ // Pinst:
7
+ // 1. POST signup-request mit valid email → 200, Mail captured durch
8
+ // sendActivationEmail-callback (echte route + signup-feature).
9
+ // 2. Resend-Idempotenz: zweiter Request für selbe email → gleicher
10
+ // Token in Mail (existing token in Redis wird re-genutzt).
11
+ // 3. POST signup-confirm mit captured Token + Password → 200, Cookies
12
+ // gesetzt (kumiko_auth + kumiko_csrf), Body mit user + tenantKey,
13
+ // DB hat user (emailVerified=true) + tenant + Admin-membership.
14
+ // 4. POST /api/auth/login mit demselben Password → 200 (Authority-
15
+ // Beweis: tenant + user + membership wirklich da, Auto-Login
16
+ // könnte stattdessen den JWT aus signup-confirm verwenden, aber
17
+ // dieser zweite Login schließt aus dass die signup-confirm-
18
+ // pipeline irgendetwas verschluckt hat).
19
+ // 5. Replay: zweiter signup-confirm mit gleichem Token → 422
20
+ // invalid_signup_token (single-use burn).
21
+ // 6. Token-not-found / abgelaufen → 422 invalid_signup_token
22
+ // (uniformer Code, kein Enumeration-leak).
23
+ // 7. Sequential Signups → unique tenantKey-Slugs (TOCTOU-Schutz
24
+ // via DB-unique-index + generateUniqueName-isAvailable-check
25
+ // zusammen).
26
+
27
+ import {
28
+ createEntityTable,
29
+ pushTables,
30
+ setupTestStack,
31
+ type TestStack,
32
+ } from "@cosmicdrift/kumiko-framework/stack";
33
+ import { eq } from "drizzle-orm";
34
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
35
+ import { createConfigFeature } from "../../config";
36
+ import { createConfigResolver } from "../../config/resolver";
37
+ import { configValuesTable } from "../../config/table";
38
+ import { createTenantFeature } from "../../tenant";
39
+ import { tenantMembershipsTable } from "../../tenant/membership-table";
40
+ import { tenantEntity, tenantTable } from "../../tenant/schema/tenant";
41
+ import { createUserFeature } from "../../user/feature";
42
+ import { userEntity, userTable } from "../../user/schema/user";
43
+ import { AuthErrors, AuthHandlers } from "../constants";
44
+ import { createAuthEmailPasswordFeature } from "../feature";
45
+
46
+ const APP_ACTIVATION_URL = "https://app.example.com/signup/complete";
47
+ const capturedActivationEmails: Array<{
48
+ email: string;
49
+ activationUrl: string;
50
+ expiresAt: string;
51
+ }> = [];
52
+
53
+ let stack: TestStack;
54
+
55
+ beforeAll(async () => {
56
+ stack = await setupTestStack({
57
+ features: [
58
+ createConfigFeature(),
59
+ createUserFeature(),
60
+ createTenantFeature(),
61
+ createAuthEmailPasswordFeature({
62
+ signup: { tokenTtlMinutes: 60 },
63
+ }),
64
+ ],
65
+ extraContext: { configResolver: createConfigResolver() },
66
+ authConfig: {
67
+ membershipQuery: "tenant:query:memberships",
68
+ loginHandler: AuthHandlers.login,
69
+ signup: {
70
+ requestHandler: AuthHandlers.signupRequest,
71
+ confirmHandler: AuthHandlers.signupConfirm,
72
+ appActivationUrl: APP_ACTIVATION_URL,
73
+ sendActivationEmail: async (args) => {
74
+ capturedActivationEmails.push(args);
75
+ },
76
+ },
77
+ },
78
+ });
79
+
80
+ await createEntityTable(stack.db, userEntity);
81
+ // tenant-entity hat den unique-constraint auf .key (siehe
82
+ // tenant.schema.indexes). createEntityTable baut das via
83
+ // buildDrizzleTable nach — pinst den TOCTOU-Schutz für signup-confirm.
84
+ await createEntityTable(stack.db, tenantEntity);
85
+ await pushTables(stack.db, { configValuesTable, tenantMembershipsTable });
86
+ });
87
+
88
+ afterAll(async () => {
89
+ await stack.cleanup();
90
+ });
91
+
92
+ beforeEach(async () => {
93
+ await stack.db.delete(userTable);
94
+ await stack.db.delete(tenantMembershipsTable);
95
+ await stack.db.delete(tenantTable);
96
+ capturedActivationEmails.length = 0;
97
+ // Redis-cleanup damit Resend-Tests keine state-leaks haben.
98
+ const allKeys = await stack.redis.redis.keys("signup:*");
99
+ if (allKeys.length > 0) await stack.redis.redis.del(...allKeys);
100
+ });
101
+
102
+ async function postSignupRequest(email: string): Promise<Response> {
103
+ return stack.http.raw("POST", "/api/auth/signup-request", { email });
104
+ }
105
+
106
+ async function postSignupConfirm(token: string, password: string): Promise<Response> {
107
+ return stack.http.raw("POST", "/api/auth/signup-confirm", { token, password });
108
+ }
109
+
110
+ async function postLogin(email: string, password: string): Promise<Response> {
111
+ return stack.http.raw("POST", "/api/auth/login", { email, password });
112
+ }
113
+
114
+ function extractTokenFromUrl(url: string): string {
115
+ const match = url.match(/[?&]token=([^&]+)/);
116
+ if (!match?.[1]) throw new Error(`No token in url: ${url}`);
117
+ return decodeURIComponent(match[1]);
118
+ }
119
+
120
+ describe("POST /api/auth/signup-request", () => {
121
+ test("known email → 200, mail captured mit activation-url", async () => {
122
+ const res = await postSignupRequest("alice@example.com");
123
+ expect(res.status).toBe(200);
124
+ expect(await res.json()).toEqual({ isSuccess: true });
125
+ expect(capturedActivationEmails).toHaveLength(1);
126
+ const [captured] = capturedActivationEmails;
127
+ if (!captured) throw new Error("no captured email");
128
+ expect(captured.email).toBe("alice@example.com");
129
+ expect(captured.activationUrl.startsWith(`${APP_ACTIVATION_URL}?token=`)).toBe(true);
130
+ expect(typeof captured.expiresAt).toBe("string");
131
+ });
132
+
133
+ test("Resend: zweiter Request für selbe email → gleicher token in Mail", async () => {
134
+ await postSignupRequest("resend@example.com");
135
+ await postSignupRequest("resend@example.com");
136
+
137
+ expect(capturedActivationEmails).toHaveLength(2);
138
+ const [first, second] = capturedActivationEmails;
139
+ if (!first || !second) throw new Error("missing emails");
140
+ expect(extractTokenFromUrl(second.activationUrl)).toBe(
141
+ extractTokenFromUrl(first.activationUrl),
142
+ );
143
+ });
144
+
145
+ test("malformed body → 200 (silent success, anti-enumeration)", async () => {
146
+ const res = await stack.http.raw("POST", "/api/auth/signup-request", { wrong: "shape" });
147
+ expect(res.status).toBe(200);
148
+ expect(capturedActivationEmails).toHaveLength(0);
149
+ });
150
+ });
151
+
152
+ describe("POST /api/auth/signup-confirm", () => {
153
+ async function requestSignup(email: string): Promise<string> {
154
+ capturedActivationEmails.length = 0;
155
+ const res = await postSignupRequest(email);
156
+ expect(res.status).toBe(200);
157
+ const captured = capturedActivationEmails[0];
158
+ if (!captured) throw new Error("signup-request fixture didn't capture mail");
159
+ return extractTokenFromUrl(captured.activationUrl);
160
+ }
161
+
162
+ test("voller Roundtrip: confirm legt user + tenant + Admin-Membership an, Cookies + Login funktioniert", async () => {
163
+ const email = "bob@example.com";
164
+ const password = "fresh-secure-pw-1234";
165
+ const token = await requestSignup(email);
166
+
167
+ const confirmRes = await postSignupConfirm(token, password);
168
+ expect(confirmRes.status).toBe(200);
169
+ const body = (await confirmRes.json()) as {
170
+ isSuccess: boolean;
171
+ token?: string;
172
+ user?: { id: string; tenantId: string; roles: string[] };
173
+ tenantKey?: string;
174
+ };
175
+ expect(body.isSuccess).toBe(true);
176
+ expect(body.token).toBeTruthy();
177
+ expect(body.user?.id).toBeTruthy();
178
+ expect(body.user?.tenantId).toBeTruthy();
179
+ expect(body.user?.roles).toContain("Admin");
180
+ expect(body.tenantKey).toBeTruthy();
181
+
182
+ // Cookies gesetzt (auth + csrf)
183
+ const setCookies = confirmRes.headers.get("set-cookie") ?? "";
184
+ expect(setCookies).toContain("kumiko_auth=");
185
+ expect(setCookies).toContain("kumiko_csrf=");
186
+
187
+ // DB-State pinst
188
+ const userRows = await stack.db.select().from(userTable).where(eq(userTable.email, email));
189
+ expect(userRows).toHaveLength(1);
190
+ expect(userRows[0]?.["emailVerified"]).toBe(true);
191
+ expect(userRows[0]?.["passwordHash"]).toBeTruthy();
192
+
193
+ const tenantRows = await stack.db
194
+ .select()
195
+ .from(tenantTable)
196
+ .where(eq(tenantTable.id, body.user?.tenantId ?? ""));
197
+ expect(tenantRows).toHaveLength(1);
198
+ expect(tenantRows[0]?.["key"]).toBe(body.tenantKey);
199
+
200
+ const memberships = await stack.db
201
+ .select()
202
+ .from(tenantMembershipsTable)
203
+ .where(eq(tenantMembershipsTable.userId, body.user?.id ?? ""));
204
+ expect(memberships).toHaveLength(1);
205
+ const rolesRaw = memberships[0]?.["roles"];
206
+ if (typeof rolesRaw === "string") {
207
+ expect(JSON.parse(rolesRaw) as string[]).toContain("Admin");
208
+ }
209
+
210
+ // Authority-Beweis: Login mit dem gesetzten Password funktioniert.
211
+ const loginRes = await postLogin(email, password);
212
+ expect(loginRes.status).toBe(200);
213
+ });
214
+
215
+ test("Single-Use-Burn: zweiter confirm mit gleichem Token → 422 invalid_signup_token", async () => {
216
+ const email = "burn@example.com";
217
+ const password = "burn-test-pw-1234";
218
+ const token = await requestSignup(email);
219
+
220
+ const first = await postSignupConfirm(token, password);
221
+ expect(first.status).toBe(200);
222
+
223
+ const second = await postSignupConfirm(token, "another-pw-9876");
224
+ expect(second.status).toBe(422);
225
+ const body = (await second.json()) as {
226
+ error?: { details?: { reason?: string } };
227
+ };
228
+ expect(body.error?.details?.reason).toBe(AuthErrors.invalidSignupToken);
229
+ });
230
+
231
+ test("unbekannter Token → 422 invalid_signup_token (Anti-Enumeration)", async () => {
232
+ const res = await postSignupConfirm("nonexistent-token-xxxxxxxxxx", "any-pw-1234");
233
+ expect(res.status).toBe(422);
234
+ const body = (await res.json()) as {
235
+ error?: { details?: { reason?: string } };
236
+ };
237
+ expect(body.error?.details?.reason).toBe(AuthErrors.invalidSignupToken);
238
+ });
239
+
240
+ test("zu kurzes Password → 400 invalid_body (Schema-Reject vor dispatcher)", async () => {
241
+ const email = "short@example.com";
242
+ const token = await requestSignup(email);
243
+ const res = await postSignupConfirm(token, "tiny");
244
+ expect(res.status).toBe(400);
245
+ });
246
+
247
+ test("mehrere sequentielle Signups → unterschiedliche tenant.key-Slugs", async () => {
248
+ const keys: string[] = [];
249
+ for (let i = 0; i < 3; i++) {
250
+ const email = `multi-${i}@example.com`;
251
+ const token = await requestSignup(email);
252
+ const confirmRes = await postSignupConfirm(token, `multi-pw-${i}-1234`);
253
+ expect(confirmRes.status).toBe(200);
254
+ const body = (await confirmRes.json()) as { tenantKey: string };
255
+ keys.push(body.tenantKey);
256
+ }
257
+ expect(new Set(keys).size).toBe(3);
258
+ });
259
+ });
@@ -0,0 +1,41 @@
1
+ // Narrow cross-feature query results (ctx.queryAs → Promise<unknown>) into
2
+ // the shape the auth handlers actually read. Replaces a bare
3
+ // `as AuthUserRow | null` at the system boundary — the coding standard
4
+ // requires a TypeGuard in place of unchecked casts from unknown.
5
+
6
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
7
+
8
+ // Fields findForAuth returns. `version` is present for updates, `email` +
9
+ // `passwordHash` only for login/reset/verification lookups. Every field
10
+ // except `id` is optional because different call-sites read different
11
+ // subsets and the projection may add nullable columns later.
12
+ export type AuthUserRow = {
13
+ readonly id: string;
14
+ readonly email?: string;
15
+ readonly version?: number;
16
+ readonly passwordHash?: string | null;
17
+ readonly isDeleted?: boolean | null;
18
+ readonly emailVerified?: boolean | null;
19
+ readonly lastActiveTenantId?: TenantId | string | null;
20
+ // JSON-encoded string[] — globale Rollen die parallel zu tenant-membership-
21
+ // roles gelten (z.B. SystemAdmin, BillingAdmin). Caller deserialisiert via
22
+ // parseRoles() vor dem Merge in die Session.
23
+ readonly roles?: string | null;
24
+ };
25
+
26
+ // Returns the narrowed row or null — mirrors findForAuth's contract where
27
+ // "not found" is a legitimate outcome (unknown email, unknown id). Throws
28
+ // NEVER — a malformed row is treated as not-found so enumeration surfaces
29
+ // stay consistent across "user doesn't exist" and "DB gave back junk".
30
+ export function parseAuthUserRow(raw: unknown): AuthUserRow | null {
31
+ if (raw === null || raw === undefined) return null;
32
+ if (typeof raw !== "object") return null;
33
+ const obj = raw as Record<string, unknown>; // @cast-boundary db-row
34
+ if (typeof obj["id"] !== "string") return null;
35
+ // Deliberate boundary-cast: id is verified; the remaining optional
36
+ // fields (email, version, isDeleted, …) are declared optional on
37
+ // AuthUserRow so a missing property at the callsite surfaces as
38
+ // `undefined` on read, not a runtime exception. Explicit validation
39
+ // of each column would duplicate findForAuth's schema and rot with it.
40
+ return obj as unknown as AuthUserRow;
41
+ }
@@ -0,0 +1,101 @@
1
+ // @runtime client
2
+ // Pure string-Konstanten — keine DB/Node-builtins. Mit `@runtime client`
3
+ // markiert damit auch Browser-Code (Members-Screen etc.) sie importieren
4
+ // kann ohne dass die runtime-isolation-Guard schreit. Runtime darf
5
+ // "client"-Files importieren (siehe RUNTIME_RULES), also bleibt auch
6
+ // der server-side Zugriff (handlers, dispatcher) erhalten.
7
+ export const AUTH_EMAIL_PASSWORD_FEATURE = "auth-email-password" as const;
8
+
9
+ // Qualified handler names. Non-CRUD handlers, no entity prefix.
10
+ export const AuthHandlers = {
11
+ login: "auth-email-password:write:login",
12
+ logout: "auth-email-password:write:logout",
13
+ changePassword: "auth-email-password:write:change-password",
14
+ requestPasswordReset: "auth-email-password:write:request-password-reset",
15
+ resetPassword: "auth-email-password:write:reset-password",
16
+ requestEmailVerification: "auth-email-password:write:request-email-verification",
17
+ verifyEmail: "auth-email-password:write:verify-email",
18
+ // Magic-Link Self-Signup (Pre-Activation-Token-Pattern). request mintet
19
+ // einen opaken Random-Token, speichert ihn bidirektional in Redis und
20
+ // sendet eine Aktivierungs-Mail. confirm löst den Token ein und legt
21
+ // user + tenant + Admin-Membership atomar an. emailVerified=true ab
22
+ // Sekunde 0 — der Klick auf den Mail-Link IST der Beweis.
23
+ signupRequest: "auth-email-password:write:signup-request",
24
+ signupConfirm: "auth-email-password:write:signup-confirm",
25
+ // Tenant-Invite Magic-Link (Admin lädt User in existing Tenant ein).
26
+ // Drei separate accept-Endpoints für klare Branch-Separation:
27
+ // inviteCreate: Admin → POST email + role
28
+ // inviteAccept: logged-in User → POST token (membership-add)
29
+ // inviteAcceptWithLogin: anon User mit existing email → POST token + email + password
30
+ // inviteSignupComplete: anon User mit neuer email → POST token + password
31
+ // inviteCancel: Admin cancelt pending invite
32
+ inviteCreate: "auth-email-password:write:invite-create",
33
+ inviteAccept: "auth-email-password:write:invite-accept",
34
+ inviteAcceptWithLogin: "auth-email-password:write:invite-accept-with-login",
35
+ inviteSignupComplete: "auth-email-password:write:invite-signup-complete",
36
+ inviteCancel: "auth-email-password:write:invite-cancel",
37
+ } as const;
38
+
39
+ // Error codes — kept intentionally generic so clients can't distinguish
40
+ // "email doesn't exist" from "password wrong". Both surface as invalid_credentials.
41
+ // Soft-deleted users also collapse into invalid_credentials to avoid enumeration.
42
+ export const AuthErrors = {
43
+ invalidCredentials: "invalid_credentials",
44
+ noMembership: "no_membership",
45
+ // Reset-flow: the route maps every reset-token verify failure (malformed,
46
+ // bad signature, expired) to this single code so a probing client can't
47
+ // learn whether a token was tampered with or just stale.
48
+ invalidResetToken: "invalid_reset_token",
49
+ resetNotConfigured: "password_reset_not_configured",
50
+ // Verification-flow: mirrors the reset-token handling. The login path
51
+ // uses `emailNotVerified` which IS a deliberate enumeration leak —
52
+ // UX benefit (explicit "check your email") outweighs the marginal
53
+ // signal ("this email exists in our system"). Signup already surfaces
54
+ // that.
55
+ invalidVerificationToken: "invalid_verification_token",
56
+ verificationNotConfigured: "email_verification_not_configured",
57
+ emailNotVerified: "email_not_verified",
58
+ // Self-Signup: alle confirm-Failures (unbekannter Token, schon
59
+ // konsumiert, abgelaufen) collapsen auf diesen Code — gleicher
60
+ // anti-enumeration-Trade-off wie reset/verify.
61
+ invalidSignupToken: "invalid_signup_token",
62
+ signupNotConfigured: "signup_not_configured",
63
+ // Invite-Flow: alle Token-Failures collapsen auf invalidInviteToken
64
+ // (anti-enumeration). emailMismatch wenn der invitee versucht den
65
+ // Link mit einer anderen Email zu accepten als die eingeladene.
66
+ invalidInviteToken: "invalid_invite_token",
67
+ inviteEmailMismatch: "invite_email_mismatch",
68
+ inviteAlreadyMember: "invite_already_member",
69
+ // Account-lockout: login refuses with this code when the user's streak of
70
+ // failed attempts has crossed the configured threshold. The error detail
71
+ // carries `retryAfterSeconds` so the UI can show a countdown. Returning a
72
+ // distinct code (rather than hiding it inside invalid_credentials) is a
73
+ // deliberate enumeration trade-off: the lockout event itself is already
74
+ // observable to the attacker, and legit users benefit from a clear signal.
75
+ accountLocked: "account_locked",
76
+ } as const;
77
+
78
+ // Account-lockout defaults — overridable via
79
+ // AuthEmailPasswordOptions.accountLockout on the feature. Defaults track the
80
+ // industry norm (NIST 800-63B) for password-only logins: a small streak
81
+ // threshold, a short cooldown.
82
+ export const AUTH_LOCKOUT_DEFAULT_MAX_FAILED_ATTEMPTS = 5;
83
+ export const AUTH_LOCKOUT_DEFAULT_DURATION_MINUTES = 15;
84
+
85
+ export const AUTH_RESET_DEFAULT_TTL_MINUTES = 15;
86
+ // Verification tokens live longer by default because the user may not be
87
+ // at their computer the moment they sign up — 24h covers "verify after
88
+ // I've got home from work". The HMAC-signed token is still single-use
89
+ // because flipping emailVerified=true is an idempotent state change:
90
+ // replaying the same token re-sets the same flag.
91
+ export const AUTH_VERIFY_DEFAULT_TTL_MINUTES = 24 * 60;
92
+
93
+ // Self-Signup: 24h Default. Lang genug damit User nicht denken muss
94
+ // "schnell aktivieren" — ein Mail-Link der morgen früh noch geht ist
95
+ // User-Friendly. Kürzere TTLs werfen Resend-Spam weil User vergessen.
96
+ export const AUTH_SIGNUP_DEFAULT_TTL_MINUTES = 24 * 60;
97
+
98
+ // Tenant-Invite: 7 Tage Default. Industry-Standard (GitHub, Linear,
99
+ // Slack); invitees brauchen oft länger zum Reagieren als bei Self-
100
+ // Signup wo die User-Intention frisch ist.
101
+ export const AUTH_INVITE_DEFAULT_TTL_MINUTES = 7 * 24 * 60;