@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,127 @@
1
+ // Tests für seedUser. Vier Invarianten:
2
+ // 1. Projection-Row landet mit email/displayName/passwordHash
3
+ // 2. Event `user.created` landet auf dem Aggregate-Stream
4
+ // 3. Idempotenz über `email` — zweiter Call liefert dieselbe userId
5
+ // ohne neuen Insert/Event
6
+ // 4. `passwordHash`-Field ist optional (User ohne Passwort, z.B. SSO-
7
+ // Federation, soll auch funktionieren)
8
+
9
+ import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
10
+ import {
11
+ createEntityTable,
12
+ pushTables,
13
+ setupTestStack,
14
+ type TestStack,
15
+ TestUsers,
16
+ } from "@cosmicdrift/kumiko-framework/stack";
17
+ import { eq } from "drizzle-orm";
18
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
19
+ import { createConfigFeature } from "../../config/feature";
20
+ import { createConfigResolver } from "../../config/resolver";
21
+ import { configValuesTable } from "../../config/table";
22
+ import { createUserFeature } from "../feature";
23
+ import { userEntity, userTable } from "../schema/user";
24
+ import { seedUser } from "../seeding";
25
+
26
+ let stack: TestStack;
27
+
28
+ beforeAll(async () => {
29
+ const resolver = createConfigResolver();
30
+ stack = await setupTestStack({
31
+ features: [createConfigFeature(), createUserFeature()],
32
+ extraContext: { configResolver: resolver },
33
+ });
34
+ await createEntityTable(stack.db, userEntity);
35
+ await pushTables(stack.db, { configValuesTable });
36
+ await createEventsTable(stack.db);
37
+ });
38
+
39
+ afterAll(async () => {
40
+ await stack.cleanup();
41
+ });
42
+
43
+ beforeEach(async () => {
44
+ await stack.db.delete(userTable);
45
+ await stack.db.delete(eventsTable);
46
+ });
47
+
48
+ describe("seedUser", () => {
49
+ test("schreibt Projection-Row mit email/displayName/passwordHash", async () => {
50
+ const userId = await seedUser(stack.db, {
51
+ email: "alice@example.com",
52
+ displayName: "Alice",
53
+ passwordHash: "$argon2id$test-hash",
54
+ });
55
+ expect(userId).toMatch(/^[0-9a-f-]{36}$/);
56
+
57
+ const rows = await stack.db
58
+ .select()
59
+ .from(userTable)
60
+ .where(eq(userTable["email"], "alice@example.com"));
61
+ expect(rows).toHaveLength(1);
62
+ expect(rows[0]?.["email"]).toBe("alice@example.com");
63
+ expect(rows[0]?.["displayName"]).toBe("Alice");
64
+ expect(rows[0]?.["passwordHash"]).toBe("$argon2id$test-hash");
65
+ });
66
+
67
+ test("emittiert user.created-Event auf den Aggregate-Stream", async () => {
68
+ const userId = await seedUser(stack.db, {
69
+ email: "bob@example.com",
70
+ displayName: "Bob",
71
+ });
72
+ const events = await stack.db
73
+ .select()
74
+ .from(eventsTable)
75
+ .where(eq(eventsTable.aggregateType, "user"));
76
+ const created = events.filter((e) => e.type === "user.created");
77
+ expect(created).toHaveLength(1);
78
+ expect(created[0]?.aggregateId).toBe(userId);
79
+ const payload = created[0]?.payload as { email: string; displayName: string };
80
+ expect(payload.email).toBe("bob@example.com");
81
+ expect(payload.displayName).toBe("Bob");
82
+ });
83
+
84
+ test("idempotent über email — zweiter Call liefert dieselbe userId, kein zweites Event", async () => {
85
+ const first = await seedUser(stack.db, {
86
+ email: "carol@example.com",
87
+ displayName: "Carol",
88
+ });
89
+ const second = await seedUser(stack.db, {
90
+ email: "carol@example.com",
91
+ displayName: "Carol Updated",
92
+ });
93
+ expect(second).toBe(first);
94
+
95
+ const rows = await stack.db
96
+ .select()
97
+ .from(userTable)
98
+ .where(eq(userTable["email"], "carol@example.com"));
99
+ expect(rows).toHaveLength(1);
100
+ // Original-displayName bleibt — zweiter Call wurde geskippt, kein update.
101
+ expect(rows[0]?.["displayName"]).toBe("Carol");
102
+
103
+ const created = await stack.db
104
+ .select()
105
+ .from(eventsTable)
106
+ .where(eq(eventsTable.aggregateType, "user"));
107
+ expect(created.filter((e) => e.type === "user.created")).toHaveLength(1);
108
+ });
109
+
110
+ test("passwordHash optional — User ohne Hash anlegbar (z.B. SSO-Federation)", async () => {
111
+ const userId = await seedUser(stack.db, {
112
+ email: "dave@example.com",
113
+ displayName: "Dave",
114
+ });
115
+ const [row] = await stack.db.select().from(userTable).where(eq(userTable["id"], userId));
116
+ expect(row?.["passwordHash"]).toBeNull();
117
+ });
118
+
119
+ test("default `by` ist TestUsers.systemAdmin (für audit-trail)", async () => {
120
+ const userId = await seedUser(stack.db, {
121
+ email: "eve@example.com",
122
+ displayName: "Eve",
123
+ });
124
+ const [row] = await stack.db.select().from(userTable).where(eq(userTable["id"], userId));
125
+ expect(row?.["insertedById"]).toBe(TestUsers.systemAdmin.id);
126
+ });
127
+ });
@@ -0,0 +1,198 @@
1
+ import {
2
+ createEntityTable,
3
+ createTestUser,
4
+ setupTestStack,
5
+ type TestStack,
6
+ TestUsers,
7
+ } from "@cosmicdrift/kumiko-framework/stack";
8
+ import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
9
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
10
+ import { UserErrors, UserHandlers, UserQueries } from "../constants";
11
+ import { createUserFeature } from "../feature";
12
+ import { userEntity, userTable } from "../schema/user";
13
+
14
+ let stack: TestStack;
15
+
16
+ const systemAdmin = TestUsers.systemAdmin;
17
+ const userFeature = createUserFeature();
18
+
19
+ beforeAll(async () => {
20
+ stack = await setupTestStack({ features: [userFeature] });
21
+ await createEntityTable(stack.db, userEntity);
22
+ });
23
+
24
+ afterAll(async () => {
25
+ await stack.cleanup();
26
+ });
27
+
28
+ beforeEach(async () => {
29
+ await stack.db.delete(userTable);
30
+ });
31
+
32
+ // Helper: create a user as SystemAdmin and return its id.
33
+ async function seedUser(overrides: {
34
+ email: string;
35
+ displayName: string;
36
+ passwordHash?: string;
37
+ }): Promise<{ id: number }> {
38
+ const res = await stack.http.writeOk<{ id: number }>(
39
+ UserHandlers.create,
40
+ {
41
+ passwordHash: "seeded-hash",
42
+ ...overrides,
43
+ },
44
+ systemAdmin,
45
+ );
46
+ return { id: res.id };
47
+ }
48
+
49
+ // --- Scenario 1: SystemAdmin creates user, me query returns correct data ---
50
+
51
+ describe("scenario 1: create + me", () => {
52
+ test("SystemAdmin creates a user, user sees their own profile via me", async () => {
53
+ const created = await seedUser({
54
+ email: "marc@example.com",
55
+ displayName: "Marc",
56
+ passwordHash: "secret-hash",
57
+ });
58
+
59
+ const signedIn = createTestUser({ id: created.id, roles: ["User"] });
60
+ const me = await stack.http.queryOk<Record<string, unknown>>(UserQueries.me, {}, signedIn);
61
+
62
+ expect(me).toMatchObject({
63
+ id: created.id,
64
+ email: "marc@example.com",
65
+ displayName: "Marc",
66
+ locale: "de", // comes from the entity's default — client didn't send it
67
+ });
68
+ });
69
+
70
+ test("normal user cannot create another user", async () => {
71
+ const normal = createTestUser({ id: 42, roles: ["User"] });
72
+ const error = await stack.http.writeErr(
73
+ UserHandlers.create,
74
+ { email: "evil@example.com", displayName: "Evil" },
75
+ normal,
76
+ );
77
+ expectErrorIncludes(error, "access_denied");
78
+ });
79
+
80
+ test("duplicate email is rejected", async () => {
81
+ await seedUser({ email: "dup@example.com", displayName: "First" });
82
+ const error = await stack.http.writeErr(
83
+ UserHandlers.create,
84
+ { email: "dup@example.com", displayName: "Second" },
85
+ systemAdmin,
86
+ );
87
+ expectErrorIncludes(error, UserErrors.emailAlreadyExists);
88
+ });
89
+ });
90
+
91
+ // --- Scenario 2: field-level read access hides passwordHash ---
92
+
93
+ describe("scenario 2: field-level read access", () => {
94
+ test("user profile does not expose passwordHash via me", async () => {
95
+ const created = await seedUser({
96
+ email: "secret@example.com",
97
+ displayName: "Secret",
98
+ passwordHash: "must-stay-hidden",
99
+ });
100
+
101
+ const signedIn = createTestUser({ id: created.id, roles: ["User"] });
102
+ const me = await stack.http.queryOk<Record<string, unknown>>(UserQueries.me, {}, signedIn);
103
+
104
+ expect(me).not.toHaveProperty("passwordHash");
105
+ // Sanity: the value is actually stored, just hidden from this role
106
+ const [row] = await stack.db.select().from(userTable);
107
+ expect((row as { passwordHash: string }).passwordHash).toBe("must-stay-hidden");
108
+ });
109
+ });
110
+
111
+ // --- Scenario 3: user edits own profile, email/passwordHash are system-locked ---
112
+
113
+ describe("scenario 3: self-update + field-level write access", () => {
114
+ test("user can change their own displayName + locale", async () => {
115
+ const created = await seedUser({ email: "editor@example.com", displayName: "Before" });
116
+ const signedIn = createTestUser({ id: created.id, roles: ["User"] });
117
+
118
+ await stack.http.writeOk(
119
+ UserHandlers.update,
120
+ { id: created.id, changes: { displayName: "After", locale: "en" }, version: 1 },
121
+ signedIn,
122
+ );
123
+
124
+ const me = await stack.http.queryOk<Record<string, unknown>>(UserQueries.me, {}, signedIn);
125
+ expect(me).toMatchObject({ displayName: "After", locale: "en" });
126
+ });
127
+
128
+ test("user cannot change their own email (field-level write-locked to system)", async () => {
129
+ const created = await seedUser({ email: "locked@example.com", displayName: "Locked" });
130
+ const signedIn = createTestUser({ id: created.id, roles: ["User"] });
131
+
132
+ const error = await stack.http.writeErr(
133
+ UserHandlers.update,
134
+ { id: created.id, changes: { email: "changed@example.com" }, version: 1 },
135
+ signedIn,
136
+ );
137
+ expectErrorIncludes(error, "field_access_denied");
138
+
139
+ // Email is unchanged in the DB
140
+ const [row] = await stack.db.select().from(userTable);
141
+ expect((row as { email: string }).email).toBe("locked@example.com");
142
+ });
143
+
144
+ test("user cannot update someone else's profile", async () => {
145
+ const victim = await seedUser({ email: "victim@example.com", displayName: "Victim" });
146
+ const attacker = createTestUser({ id: victim.id + 1000, roles: ["User"] });
147
+
148
+ const error = await stack.http.writeErr(
149
+ UserHandlers.update,
150
+ { id: victim.id, changes: { displayName: "Pwned" }, version: 1 },
151
+ attacker,
152
+ );
153
+ expectErrorIncludes(error, UserErrors.cannotEditOtherUser);
154
+ });
155
+ });
156
+
157
+ // --- Scenario 4: detail + list are SystemAdmin-only ---
158
+
159
+ describe("scenario 4: detail + list access", () => {
160
+ test("SystemAdmin can fetch any user via detail", async () => {
161
+ const target = await seedUser({ email: "target@example.com", displayName: "Target" });
162
+
163
+ const detail = await stack.http.queryOk<Record<string, unknown>>(
164
+ UserQueries.detail,
165
+ { id: target.id },
166
+ systemAdmin,
167
+ );
168
+
169
+ expect(detail).toMatchObject({ id: target.id, email: "target@example.com" });
170
+ });
171
+
172
+ test("tenant Admin cannot fetch arbitrary users (role leak guard)", async () => {
173
+ const target = await seedUser({ email: "other@example.com", displayName: "Other" });
174
+ const tenantAdmin = createTestUser({ id: 9999, roles: ["Admin"] });
175
+
176
+ const res = await stack.http.query(UserQueries.detail, { id: target.id }, tenantAdmin);
177
+ expect(res.status).toBe(403);
178
+ });
179
+
180
+ test("list returns users (SystemAdmin only)", async () => {
181
+ await seedUser({ email: "a@example.com", displayName: "A" });
182
+ await seedUser({ email: "b@example.com", displayName: "B" });
183
+
184
+ const result = await stack.http.queryOk<{ rows: Record<string, unknown>[] }>(
185
+ UserQueries.list,
186
+ {},
187
+ systemAdmin,
188
+ );
189
+
190
+ expect(result.rows.length).toBeGreaterThanOrEqual(2);
191
+ });
192
+
193
+ test("normal user cannot list", async () => {
194
+ const signedIn = createTestUser({ id: 2000, roles: ["User"] });
195
+ const res = await stack.http.query(UserQueries.list, {}, signedIn);
196
+ expect(res.status).toBe(403);
197
+ });
198
+ });
@@ -0,0 +1,15 @@
1
+ // Command-input schemas for the user write handlers, re-exposed for external
2
+ // consumers — primarily migration mappers that need to write events directly
3
+ // into the core `user` stream via `eventStore.appendRaw` (Marten-bypass) and
4
+ // must validate their payloads against the exact handler contract.
5
+ //
6
+ // See `tenant/command-schemas.ts` for the same pattern + the schema-vs-event-
7
+ // payload caveat (strip-id, defaults, sensitive, compound-type flattening).
8
+
9
+ import { createWrite } from "./handlers/create.write";
10
+ import { updateWrite } from "./handlers/update.write";
11
+
12
+ export const UserCommandSchemas = {
13
+ create: createWrite.schema,
14
+ update: updateWrite.schema,
15
+ } as const;
@@ -0,0 +1,23 @@
1
+ // Feature name
2
+ export const USER_FEATURE = "user" as const;
3
+
4
+ // Qualified write handler names. Handlers carry the "user:" entity prefix so
5
+ // field-level access rules (passwordHash system-only etc.) are wired up.
6
+ export const UserHandlers = {
7
+ create: "user:write:user:create",
8
+ update: "user:write:user:update",
9
+ } as const;
10
+
11
+ // Qualified query handler names
12
+ export const UserQueries = {
13
+ me: "user:query:user:me",
14
+ detail: "user:query:user:detail",
15
+ list: "user:query:user:list",
16
+ findForAuth: "user:query:user:find-for-auth",
17
+ } as const;
18
+
19
+ // Error codes
20
+ export const UserErrors = {
21
+ emailAlreadyExists: "email_already_exists",
22
+ cannotEditOtherUser: "cannot_edit_other_user",
23
+ } as const;
@@ -0,0 +1,32 @@
1
+ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { createWrite } from "./handlers/create.write";
3
+ import { detailQuery } from "./handlers/detail.query";
4
+ import { findForAuthQuery } from "./handlers/find-for-auth.query";
5
+ import { listQuery } from "./handlers/list.query";
6
+ import { meQuery } from "./handlers/me.query";
7
+ import { updateWrite } from "./handlers/update.write";
8
+ import { userEntity } from "./schema/user";
9
+
10
+ // The user feature holds the cross-tenant user identity. `systemScope()` means
11
+ // queries and writes bypass the tenant filter — a user exists above any tenant.
12
+ // Membership + tenant-specific roles live in the tenant feature.
13
+ export function createUserFeature(): FeatureDefinition {
14
+ return defineFeature("user", (r) => {
15
+ r.systemScope();
16
+ r.entity("user", userEntity);
17
+
18
+ const handlers = {
19
+ create: r.writeHandler(createWrite),
20
+ update: r.writeHandler(updateWrite),
21
+ };
22
+
23
+ const queries = {
24
+ me: r.queryHandler(meQuery),
25
+ detail: r.queryHandler(detailQuery),
26
+ list: r.queryHandler(listQuery),
27
+ findForAuth: r.queryHandler(findForAuthQuery),
28
+ };
29
+
30
+ return { handlers, queries };
31
+ });
32
+ }
@@ -0,0 +1,54 @@
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { ConflictError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { eq } from "drizzle-orm";
5
+ import { z } from "zod";
6
+ import { UserErrors } from "../constants";
7
+ import { userEntity, userTable } from "../schema/user";
8
+
9
+ const crud = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
10
+
11
+ // Only the Auth features (running as SYSTEM) or a SystemAdmin may create users.
12
+ //
13
+ // Email uniqueness is checked via a pre-flight query — the framework has no
14
+ // `unique:` field flag yet. This check is race-prone: two concurrent requests
15
+ // can both see "no duplicate" and both insert. Acceptable MVP behavior since
16
+ // user creation is low-frequency and gated by privileged roles; the DB will
17
+ // still surface a pg unique violation once we add the constraint.
18
+ // TODO: replace with a real `unique:` field flag + DB constraint.
19
+ export const createWrite = defineWriteHandler({
20
+ name: "user:create",
21
+ schema: z.object({
22
+ email: z.email(),
23
+ passwordHash: z.string().optional(),
24
+ displayName: z.string().min(1).max(100),
25
+ locale: z.string().min(2).max(10).optional(),
26
+ // Globale Rollen — JSON-encoded string[]. Optional weil der Default
27
+ // im Entity-Schema "[]" ist; setzen wenn man einen SystemAdmin (oder
28
+ // andere globale Rollen) anlegt. Field-Access (write: privileged) auf
29
+ // der Entity ist die letzte Hand: wer auch immer create dispatcht ist
30
+ // schon privileged (system/SystemAdmin), aber das Field-Guard läuft
31
+ // trotzdem als defense-in-depth.
32
+ roles: z.string().optional(),
33
+ }),
34
+ access: { roles: ["system", "SystemAdmin"] },
35
+ handler: async (event, ctx) => {
36
+ const existing = await ctx.db
37
+ .select({ id: userTable["id"] })
38
+ .from(userTable)
39
+ .where(eq(userTable["email"], event.payload.email))
40
+ .limit(1);
41
+
42
+ if (existing.length > 0) {
43
+ return writeFailure(
44
+ new ConflictError({
45
+ message: "email already exists",
46
+ i18nKey: "user.errors.emailAlreadyExists",
47
+ details: { reason: UserErrors.emailAlreadyExists, field: "email" },
48
+ }),
49
+ );
50
+ }
51
+
52
+ return crud.create(event.payload, event.user, ctx.db);
53
+ },
54
+ });
@@ -0,0 +1,9 @@
1
+ import { access, defineEntityDetailHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { userEntity } from "../schema/user";
3
+
4
+ // Only SystemAdmins can read arbitrary users. Tenant-level "Admin" does NOT
5
+ // grant this — the user feature is tenant-agnostic, and an Admin's scope is
6
+ // bound to their own tenant's memberships (served by the tenant feature).
7
+ export const detailQuery = defineEntityDetailHandler("user", userEntity, {
8
+ access: { roles: access.systemAdmin },
9
+ });
@@ -0,0 +1,38 @@
1
+ import type { DbRow } from "@cosmicdrift/kumiko-framework/db";
2
+ import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+ import { userTable } from "../schema/user";
6
+
7
+ // Privileged auth lookup: returns the full user row — including passwordHash —
8
+ // by email OR id (exactly one, enforced by the schema). Used by the auth
9
+ // features via ctx.queryAs(systemUser, ...).
10
+ //
11
+ // Field-level read rules allow passwordHash for the "privileged" role set,
12
+ // so system callers see everything; any other caller is filtered even if
13
+ // they somehow reach this handler. Access is also restricted to privileged
14
+ // — regular users or tenant admins cannot call this at all.
15
+ export const findForAuthQuery = defineQueryHandler({
16
+ name: "user:find-for-auth",
17
+ schema: z
18
+ .object({
19
+ email: z.email().optional(),
20
+ id: z.uuid().optional(),
21
+ })
22
+ .refine(
23
+ // XOR: exactly one must be set. Neither or both is a caller bug, not an
24
+ // ambiguous lookup.
25
+ (v) => (v.email !== undefined) !== (v.id !== undefined),
26
+ { message: "exactly one of email or id must be set" },
27
+ ),
28
+ access: { roles: access.privileged },
29
+ handler: async (query, ctx) => {
30
+ const condition =
31
+ query.payload.email !== undefined
32
+ ? eq(userTable["email"], query.payload.email)
33
+ : eq(userTable["id"], query.payload.id as string);
34
+
35
+ const [row] = await ctx.db.select().from(userTable).where(condition).limit(1);
36
+ return (row as DbRow) ?? null;
37
+ },
38
+ });
@@ -0,0 +1,8 @@
1
+ import { access, defineEntityListHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { userEntity } from "../schema/user";
3
+
4
+ // System-wide user listing is SystemAdmin-only. Tenant admins list their
5
+ // members via the tenant feature (which scopes by membership, not globally).
6
+ export const listQuery = defineEntityListHandler("user", userEntity, {
7
+ access: { roles: access.systemAdmin },
8
+ });
@@ -0,0 +1,15 @@
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { z } from "zod";
4
+ import { userEntity, userTable } from "../schema/user";
5
+
6
+ const crud = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
7
+
8
+ // Returns the currently signed-in user's profile. Field-level read access
9
+ // strips out the passwordHash automatically (configured on the entity).
10
+ export const meQuery = defineQueryHandler({
11
+ name: "user:me",
12
+ schema: z.object({}),
13
+ access: { openToAll: true },
14
+ handler: async (query, ctx) => crud.detail({ id: query.user.id }, query.user, ctx.db),
15
+ });
@@ -0,0 +1,54 @@
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
+ import { access, defineWriteHandler, hasAccess } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { AccessDeniedError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { z } from "zod";
5
+ import { UserErrors } from "../constants";
6
+ import { userEntity, userTable } from "../schema/user";
7
+
8
+ const crud = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
9
+
10
+ // Users can update their OWN profile; SystemAdmin/system can update anyone.
11
+ // Handler-level access is openToAll — the row guard below is the actual gate,
12
+ // and field-level access (passwordHash/email write-locked to "privileged")
13
+ // stops any write that shouldn't touch an identity column.
14
+ export const updateWrite = defineWriteHandler({
15
+ name: "user:update",
16
+ schema: z.object({
17
+ id: z.uuid(),
18
+ // Clients must send the version they read. The CrudExecutor rejects
19
+ // missing versions with version_conflict — see optimistic-locking in
20
+ // crud-executor.ts.
21
+ version: z.number(),
22
+ changes: z.object({
23
+ displayName: z.string().min(1).max(100).optional(),
24
+ locale: z.string().min(2).max(10).optional(),
25
+ email: z.email().optional(),
26
+ passwordHash: z.string().optional(),
27
+ lastActiveTenantId: z.string().optional(),
28
+ emailVerified: z.boolean().optional(),
29
+ // Globale Rollen — JSON-encoded string[]. Field-level write-access
30
+ // ist privileged (siehe userEntity.roles), d.h. ein non-privileged
31
+ // Caller sieht hier zwar einen 200, aber das Field-Guard im
32
+ // executor blockt die Spalte vorm Schreiben (silent strip). Schema
33
+ // akzeptiert das field damit der SystemAdmin-Pfad explizit
34
+ // existiert; der Privilege-Escalation-Schutz greift im
35
+ // FieldAccessFilter, nicht im Schema.
36
+ roles: z.string().optional(),
37
+ }),
38
+ }),
39
+ access: { openToAll: true },
40
+ handler: async (event, ctx) => {
41
+ const isSelf = event.payload.id === event.user.id;
42
+ const isPrivileged = hasAccess(event.user, { roles: access.privileged });
43
+ if (!isSelf && !isPrivileged) {
44
+ return writeFailure(
45
+ new AccessDeniedError({
46
+ message: "cannot edit other user",
47
+ i18nKey: "user.errors.cannotEditOtherUser",
48
+ details: { reason: UserErrors.cannotEditOtherUser, targetUserId: event.payload.id },
49
+ }),
50
+ );
51
+ }
52
+ return crud.update(event.payload, event.user, ctx.db);
53
+ },
54
+ });
@@ -0,0 +1,4 @@
1
+ export { UserCommandSchemas } from "./command-schemas";
2
+ export { USER_FEATURE, UserErrors, UserHandlers, UserQueries } from "./constants";
3
+ export { createUserFeature } from "./feature";
4
+ export { userEntity, userTable } from "./schema/user";
@@ -0,0 +1,5 @@
1
+ // Re-exports aus den schema/<entity>.ts Files. Eine Datei pro Entity
2
+ // — wenn das Feature später weitere Entities hinzubekommt, kommen sie
3
+ // als zusätzliche Datei rein und werden hier exportiert.
4
+
5
+ export * from "./user";
@@ -0,0 +1,69 @@
1
+ import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
2
+ import {
3
+ access,
4
+ createBooleanField,
5
+ createEntity,
6
+ createTextField,
7
+ } from "@cosmicdrift/kumiko-framework/engine";
8
+
9
+ // User entity — tenant-agnostic. A single user can belong to multiple tenants
10
+ // via tenantMemberships. No tenantId column on this table.
11
+ export const userEntity = createEntity({
12
+ table: "read_users",
13
+ softDelete: true,
14
+ fields: {
15
+ // Identity — anyone who can see the user can read the email, but only
16
+ // privileged roles (SYSTEM auth code, SystemAdmin) may change it.
17
+ email: createTextField({
18
+ required: true,
19
+ format: "email",
20
+ maxLength: 320,
21
+ access: { write: access.privileged },
22
+ }),
23
+
24
+ // Password material: only SYSTEM/SystemAdmin can read or write it.
25
+ // auth-email-password reads it during login, writes it during registration
26
+ // and password changes. Stripped from ordinary responses via read-access.
27
+ passwordHash: createTextField({
28
+ maxLength: 255,
29
+ access: { read: access.privileged, write: access.privileged },
30
+ }),
31
+
32
+ // Profile — user-editable
33
+ displayName: createTextField({ required: true, maxLength: 100, searchable: true }),
34
+ locale: createTextField({ maxLength: 10, default: "de" }),
35
+
36
+ // Which tenant should this user land in on next login. Set by the login
37
+ // handler (SYSTEM), read by the login flow + UI for deep-linking.
38
+ // UUID string matching tenants.id; createTextField stores it as text.
39
+ lastActiveTenantId: createTextField({
40
+ maxLength: 36,
41
+ access: { write: access.privileged },
42
+ }),
43
+
44
+ // Email-verification flag — flipped to true by the verify-email handler
45
+ // after an HMAC-signed token roundtrip. Readable by anyone who can see
46
+ // the user row; writable only by privileged (system) callers so a user
47
+ // can't self-mark themselves verified. Login can be config-gated to
48
+ // refuse a session while this is false (strict mode).
49
+ emailVerified: createBooleanField({
50
+ default: false,
51
+ access: { write: access.privileged },
52
+ }),
53
+
54
+ // Globale Rollen — parallel zu tenantMemberships.roles. JSON-encoded
55
+ // string[]; parseRoles() deserialisiert beim Read. Login-Handler mergt
56
+ // diese Rollen mit den tenant-membership-roles in die Session — so
57
+ // sind sie tenant-unabhängig (z.B. SystemAdmin, BillingAdmin). Default
58
+ // "[]" damit die Session-Roles-Merge keinen NULL-Branch braucht.
59
+ // Schreibrecht privileged: ein User darf sich nicht selbst zum
60
+ // SystemAdmin machen.
61
+ roles: createTextField({
62
+ required: true,
63
+ default: "[]",
64
+ access: { write: access.privileged },
65
+ }),
66
+ },
67
+ });
68
+
69
+ export const userTable = buildDrizzleTable("user", userEntity);