@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,175 @@
1
+ // Integration test for the sessions cleanup job. Pattern mirrors
2
+ // secrets/retention.integration.ts — we hit the handler directly with a
3
+ // minimal ctx, because the full setupTestStack + jobRunner path is
4
+ // exercised by the framework's job tests. Here we pin the semantics: old
5
+ // expired/revoked rows go, live rows stay, batching + signal work.
6
+
7
+ import type { AppContext } from "@cosmicdrift/kumiko-framework/engine";
8
+ import {
9
+ createEntityTable,
10
+ setupTestStack,
11
+ type TestStack,
12
+ testTenantId,
13
+ } from "@cosmicdrift/kumiko-framework/stack";
14
+ import { sql } from "drizzle-orm";
15
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
16
+ import { createSessionsFeature } from "../feature";
17
+ import { cleanupJob } from "../handlers/cleanup.job";
18
+ import { userSessionEntity, userSessionTable } from "../schema/user-session";
19
+
20
+ type Log = NonNullable<AppContext["log"]>;
21
+ function silentLogger(): Log {
22
+ const noop = () => {};
23
+ const logger: Log = {
24
+ info: noop,
25
+ warn: noop,
26
+ error: noop,
27
+ debug: noop,
28
+ child: () => logger,
29
+ };
30
+ return logger;
31
+ }
32
+
33
+ const TENANT = testTenantId(1);
34
+
35
+ let stack: TestStack;
36
+
37
+ beforeAll(async () => {
38
+ stack = await setupTestStack({
39
+ features: [createSessionsFeature()],
40
+ });
41
+ await createEntityTable(stack.db, userSessionEntity);
42
+ });
43
+
44
+ afterAll(async () => {
45
+ await stack.cleanup();
46
+ });
47
+
48
+ beforeEach(async () => {
49
+ await stack.db.delete(userSessionTable);
50
+ });
51
+
52
+ type JobCtx = Pick<AppContext, "db" | "registry" | "log">;
53
+ function jobCtx(): Parameters<typeof cleanupJob>[1] {
54
+ const ctx: JobCtx = {
55
+ db: stack.db,
56
+ registry: stack.registry,
57
+ log: silentLogger(),
58
+ };
59
+ return ctx as unknown as Parameters<typeof cleanupJob>[1];
60
+ }
61
+
62
+ // Seed a session row at a specific age. `kind` picks which lifecycle column
63
+ // to back-date: "expired" sets expiresAt in the past (session lived out its
64
+ // window), "revoked" sets revokedAt (user logged out, time passed), "live"
65
+ // leaves the row current (should never be deleted).
66
+ async function seedSession(opts: {
67
+ id: string;
68
+ userId: string;
69
+ kind: "live" | "expired" | "revoked";
70
+ ageDays: number;
71
+ }): Promise<void> {
72
+ const now = sql`now()`;
73
+ const pastCreated = sql`now() - ${sql.raw(`interval '${opts.ageDays + 1} days'`)}`;
74
+ const past = sql`now() - ${sql.raw(`interval '${opts.ageDays} days'`)}`;
75
+ const future = sql`now() + ${sql.raw(`interval '30 days'`)}`;
76
+
77
+ await stack.db.insert(userSessionTable).values({
78
+ id: opts.id,
79
+ tenantId: TENANT,
80
+ userId: opts.userId,
81
+ createdAt: pastCreated,
82
+ expiresAt: opts.kind === "expired" ? past : future,
83
+ revokedAt: opts.kind === "revoked" ? past : null,
84
+ ip: "test",
85
+ userAgent: "test",
86
+ modifiedAt: now,
87
+ });
88
+ }
89
+
90
+ async function countSessions(): Promise<number> {
91
+ const rows = await stack.db.select().from(userSessionTable);
92
+ return rows.length;
93
+ }
94
+
95
+ describe("sessions cleanup job — purge expired/revoked rows", () => {
96
+ test("deletes expired-past-cutoff rows but keeps live ones", async () => {
97
+ await seedSession({
98
+ id: "11111111-1111-1111-1111-111111111111",
99
+ userId: "aa000000-0000-0000-0000-000000000001",
100
+ kind: "expired",
101
+ ageDays: 45,
102
+ });
103
+ await seedSession({
104
+ id: "22222222-2222-2222-2222-222222222222",
105
+ userId: "aa000000-0000-0000-0000-000000000002",
106
+ kind: "live",
107
+ ageDays: 1,
108
+ });
109
+ expect(await countSessions()).toBe(2);
110
+
111
+ await cleanupJob({}, jobCtx());
112
+
113
+ expect(await countSessions()).toBe(1);
114
+ const [remaining] = await stack.db.select().from(userSessionTable);
115
+ expect(remaining?.["revokedAt"]).toBeNull();
116
+ });
117
+
118
+ test("deletes long-revoked rows", async () => {
119
+ await seedSession({
120
+ id: "33333333-3333-3333-3333-333333333333",
121
+ userId: "bb000000-0000-0000-0000-000000000001",
122
+ kind: "revoked",
123
+ ageDays: 60,
124
+ });
125
+ expect(await countSessions()).toBe(1);
126
+
127
+ await cleanupJob({}, jobCtx());
128
+
129
+ expect(await countSessions()).toBe(0);
130
+ });
131
+
132
+ test("recently-revoked rows stay around (inside retention window)", async () => {
133
+ await seedSession({
134
+ id: "44444444-4444-4444-4444-444444444444",
135
+ userId: "cc000000-0000-0000-0000-000000000001",
136
+ kind: "revoked",
137
+ ageDays: 10,
138
+ });
139
+
140
+ // Default 30d window: 10d-old revoked row stays
141
+ await cleanupJob({}, jobCtx());
142
+
143
+ expect(await countSessions()).toBe(1);
144
+ });
145
+
146
+ test("olderThanDays override respects custom windows", async () => {
147
+ await seedSession({
148
+ id: "55555555-5555-5555-5555-555555555555",
149
+ userId: "dd000000-0000-0000-0000-000000000001",
150
+ kind: "revoked",
151
+ ageDays: 5,
152
+ });
153
+
154
+ // Tight 3d window: the 5d row goes
155
+ await cleanupJob({ olderThanDays: 3 }, jobCtx());
156
+
157
+ expect(await countSessions()).toBe(0);
158
+ });
159
+
160
+ test("batching drains a large backlog across chunks", async () => {
161
+ for (let i = 0; i < 7; i++) {
162
+ await seedSession({
163
+ id: `99999999-9999-9999-9999-${String(i).padStart(12, "0")}`,
164
+ userId: "ee000000-0000-0000-0000-000000000001",
165
+ kind: "expired",
166
+ ageDays: 60,
167
+ });
168
+ }
169
+ expect(await countSessions()).toBe(7);
170
+
171
+ await cleanupJob({ olderThanDays: 30, batchSize: 2 }, jobCtx());
172
+
173
+ expect(await countSessions()).toBe(0);
174
+ });
175
+ });
@@ -0,0 +1,202 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import {
5
+ createEntityTable,
6
+ pushTables,
7
+ setupTestStack,
8
+ type TestStack,
9
+ testTenantId,
10
+ } from "@cosmicdrift/kumiko-framework/stack";
11
+ import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
12
+ import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
13
+ import { AuthHandlers } from "../../auth-email-password/constants";
14
+ import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
15
+ import { createConfigFeature } from "../../config";
16
+ import { createConfigResolver } from "../../config/resolver";
17
+ import { configValuesTable } from "../../config/table";
18
+ import { createTenantFeature } from "../../tenant";
19
+ import { tenantMembershipsTable } from "../../tenant/membership-table";
20
+ import { tenantEntity } from "../../tenant/schema/tenant";
21
+ import { UserHandlers } from "../../user";
22
+ import { createUserFeature } from "../../user/feature";
23
+ import { userEntity, userTable } from "../../user/schema/user";
24
+ import { createSessionsFeature } from "../feature";
25
+ import { userSessionEntity, userSessionTable } from "../schema/user-session";
26
+ import { createSessionCallbacks, type SessionCallbacks } from "../session-callbacks";
27
+ import { sessionCallbacksFromLateBound } from "../testing";
28
+ import { makeSessionHelpers } from "./test-helpers";
29
+
30
+ // When a user changes their password, every live session for that user must
31
+ // stop working — the industry-standard "signs you out everywhere" rule.
32
+ // Proves the sessions-feature wires the user-entity postSave hook correctly
33
+ // and the mass-revoker does the full sweep (including the caller's session).
34
+
35
+ let stack: TestStack;
36
+ let h: ReturnType<typeof makeSessionHelpers>;
37
+ const callbacks = createLateBoundHolder<SessionCallbacks>("session-callbacks");
38
+
39
+ // vi.fn spy for the revoker — lets us assert exact call counts and arguments
40
+ // per test without leaking module-level mutable state across suites.
41
+ const massRevokeSpy = vi.fn<(userId: string) => Promise<number>>();
42
+
43
+ const encryptionKey = randomBytes(32).toString("base64");
44
+
45
+ // Align with TestUsers.systemAdmin.tenantId so seed + change-password write
46
+ // events onto the same stream. Mismatched tenants land create on A and
47
+ // update on B — getStreamVersion returns 0 and optimistic-lock fails.
48
+ const TENANT: TenantId = testTenantId(1);
49
+
50
+ beforeAll(async () => {
51
+ const encryption = createEncryptionProvider(encryptionKey);
52
+ const resolver = createConfigResolver({ encryption });
53
+ const bound = sessionCallbacksFromLateBound(callbacks);
54
+ const baseRevoker = bound.asMassRevoker();
55
+
56
+ // Wire the spy as the revoker passed to the feature; it forwards to the
57
+ // real one so the DB stays in sync, but also records the call.
58
+ massRevokeSpy.mockImplementation((userId) => baseRevoker(userId));
59
+
60
+ stack = await setupTestStack({
61
+ features: [
62
+ createConfigFeature(),
63
+ createUserFeature(),
64
+ createTenantFeature(),
65
+ createAuthEmailPasswordFeature(),
66
+ createSessionsFeature({
67
+ autoRevokeOnPasswordChange: massRevokeSpy,
68
+ }),
69
+ ],
70
+ extraContext: { configResolver: resolver, configEncryption: encryption },
71
+ authConfig: {
72
+ ...bound.asAuthConfig(),
73
+ membershipQuery: "tenant:query:memberships",
74
+ loginHandler: AuthHandlers.login,
75
+ },
76
+ });
77
+ callbacks.set(createSessionCallbacks({ db: stack.db }));
78
+ h = makeSessionHelpers(stack, TENANT);
79
+
80
+ await createEntityTable(stack.db, userEntity);
81
+ await createEntityTable(stack.db, tenantEntity);
82
+ await createEntityTable(stack.db, userSessionEntity);
83
+ await pushTables(stack.db, { configValuesTable, tenantMembershipsTable });
84
+ });
85
+
86
+ afterAll(async () => {
87
+ await stack.cleanup();
88
+ });
89
+
90
+ beforeEach(async () => {
91
+ await stack.db.delete(userTable);
92
+ await stack.db.delete(tenantMembershipsTable);
93
+ await stack.db.delete(userSessionTable);
94
+ massRevokeSpy.mockClear();
95
+ });
96
+
97
+ describe("password change mass-revokes every live session", () => {
98
+ test("changing the password revokes ALL sessions including the caller's", async () => {
99
+ await h.seedUser("rotate@example.com", "first-password");
100
+
101
+ const a = await h.login("rotate@example.com", "first-password");
102
+ const b = await h.login("rotate@example.com", "first-password");
103
+ const c = await h.login("rotate@example.com", "first-password");
104
+
105
+ // Sanity: all three are currently live and queries go through
106
+ expect(
107
+ (await h.authedPost("/api/query", a.token, { type: "user:query:user:me", payload: {} }))
108
+ .status,
109
+ ).toBe(200);
110
+ expect(
111
+ (await h.authedPost("/api/query", b.token, { type: "user:query:user:me", payload: {} }))
112
+ .status,
113
+ ).toBe(200);
114
+ expect(
115
+ (await h.authedPost("/api/query", c.token, { type: "user:query:user:me", payload: {} }))
116
+ .status,
117
+ ).toBe(200);
118
+
119
+ // b changes the password via its own JWT
120
+ const change = await h.authedPost("/api/write", b.token, {
121
+ type: AuthHandlers.changePassword,
122
+ payload: { oldPassword: "first-password", newPassword: "second-password-long" },
123
+ });
124
+ expect(change.status).toBe(200);
125
+
126
+ // Revoker was called exactly once; the return-value reports the 3 live
127
+ // sessions it revoked (a + b + c, all for the same user).
128
+ expect(massRevokeSpy).toHaveBeenCalledTimes(1);
129
+ expect(await massRevokeSpy.mock.results[0]?.value).toBe(3);
130
+
131
+ // Every previously-live session — INCLUDING b — is now revoked
132
+ expect(
133
+ (await h.authedPost("/api/query", a.token, { type: "user:query:user:me", payload: {} }))
134
+ .status,
135
+ ).toBe(401);
136
+ expect(
137
+ (await h.authedPost("/api/query", b.token, { type: "user:query:user:me", payload: {} }))
138
+ .status,
139
+ ).toBe(401);
140
+ expect(
141
+ (await h.authedPost("/api/query", c.token, { type: "user:query:user:me", payload: {} }))
142
+ .status,
143
+ ).toBe(401);
144
+
145
+ // DB state confirms: zero live rows for this user
146
+ const liveRows = await stack.db.select().from(userSessionTable);
147
+ const stillLive = liveRows.filter((r) => r["revokedAt"] === null);
148
+ expect(stillLive).toHaveLength(0);
149
+
150
+ // And logging in again with the NEW password works
151
+ const loginAfter = await stack.http.raw("POST", "/api/auth/login", {
152
+ email: "rotate@example.com",
153
+ password: "second-password-long",
154
+ });
155
+ expect(loginAfter.status).toBe(200);
156
+ });
157
+
158
+ test("user:create does NOT trigger mass-revoke (isNew guard)", async () => {
159
+ // seedUser does a user:create — the hook fires, but the isNew guard
160
+ // should short-circuit before the mass-revoker runs. A future refactor
161
+ // that drops the guard would make the spy show a call here.
162
+ await h.seedUser("fresh@example.com", "pw-long-enough");
163
+ expect(massRevokeSpy).not.toHaveBeenCalled();
164
+ });
165
+
166
+ test("editing a non-password field does NOT trigger mass-revoke", async () => {
167
+ const { userId } = await h.seedUser("stable@example.com", "pw-long-enough");
168
+ const a = await h.login("stable@example.com", "pw-long-enough");
169
+
170
+ // Grab the version number so the user:update handler passes the
171
+ // optimistic-lock check. `me` returns the current row to the caller
172
+ // with version included.
173
+ const meRes = await h.authedPost("/api/query", a.token, {
174
+ type: "user:query:user:me",
175
+ payload: {},
176
+ });
177
+ expect(meRes.status).toBe(200);
178
+ const me = (await meRes.json()) as { data: { version: number } };
179
+
180
+ // The caller updates their own displayName — must NOT sign them out.
181
+ const update = await h.authedPost("/api/write", a.token, {
182
+ type: UserHandlers.update,
183
+ payload: {
184
+ id: userId,
185
+ version: me.data.version,
186
+ changes: { displayName: "New Name" },
187
+ },
188
+ });
189
+ expect(update.status).toBe(200);
190
+
191
+ // Revoker not called — the isNew guard + passwordHash guard both must
192
+ // have held off.
193
+ expect(massRevokeSpy).not.toHaveBeenCalled();
194
+
195
+ // Same JWT still works
196
+ const after = await h.authedPost("/api/query", a.token, {
197
+ type: "user:query:user:me",
198
+ payload: {},
199
+ });
200
+ expect(after.status).toBe(200);
201
+ });
202
+ });