@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,23 @@
1
+ // Feature name
2
+ export const CONFIG_FEATURE = "config" as const;
3
+
4
+ // Qualified write handler names (QN format: scope:type:name)
5
+ export const ConfigHandlers = {
6
+ set: "config:write:set",
7
+ reset: "config:write:reset",
8
+ } as const;
9
+
10
+ // Qualified query handler names (QN format: scope:type:name)
11
+ export const ConfigQueries = {
12
+ values: "config:query:values",
13
+ schema: "config:query:schema",
14
+ } as const;
15
+
16
+ // Error codes
17
+ export const ConfigErrors = {
18
+ unknownKey: "unknown_config_key",
19
+ systemOnly: "config_key_is_system_only",
20
+ invalidScope: "invalid_scope",
21
+ typeError: "type_error",
22
+ invalidOption: "invalid_option",
23
+ } as const;
@@ -0,0 +1,117 @@
1
+ import type { DbConnection, EncryptionProvider, TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import {
3
+ type ConfigAccessor,
4
+ type ConfigAccessorFactory,
5
+ type ConfigKeyHandle,
6
+ type ConfigKeyType,
7
+ type ConfigValue,
8
+ defineFeature,
9
+ type FeatureDefinition,
10
+ type HandlerContext,
11
+ type Registry,
12
+ type TenantId,
13
+ } from "@cosmicdrift/kumiko-framework/engine";
14
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
15
+ import { resetWrite } from "./handlers/reset.write";
16
+ import { schemaQuery } from "./handlers/schema.query";
17
+ import { setWrite } from "./handlers/set.write";
18
+ import { valuesQuery } from "./handlers/values.query";
19
+ import type { ConfigResolver } from "./resolver";
20
+ import { configValueEntity } from "./table";
21
+
22
+ export type ConfigContext = { readonly config: ConfigAccessor };
23
+
24
+ export function createConfigFeature(): FeatureDefinition {
25
+ return defineFeature("config", (r) => {
26
+ r.systemScope();
27
+
28
+ // One aggregate stream per (key, scope) pair — the executor handles the
29
+ // lifecycle events `configValue.created / .updated / .deleted` plus the
30
+ // projection write in one TX. Subscribers that need config-change
31
+ // semantics listen to those auto-events via r.multiStreamProjection
32
+ // (see docs/plans/architecture/event-sourcing-pivot.md §4.7).
33
+ r.entity("config-value", configValueEntity);
34
+
35
+ const handlers = {
36
+ set: r.writeHandler(setWrite),
37
+ reset: r.writeHandler(resetWrite),
38
+ };
39
+
40
+ const queries = {
41
+ values: r.queryHandler(valuesQuery),
42
+ schema: r.queryHandler(schemaQuery),
43
+ };
44
+
45
+ return { handlers, queries };
46
+ });
47
+ }
48
+
49
+ export function createConfigAccessor(
50
+ registry: Registry,
51
+ resolver: ConfigResolver,
52
+ tenantId: TenantId,
53
+ userId: string,
54
+ db: DbConnection | TenantDb,
55
+ ): ConfigAccessor {
56
+ async function configAccessor(
57
+ qualifiedKey: string,
58
+ ): Promise<string | number | boolean | undefined>;
59
+ async function configAccessor<T extends ConfigKeyType>(
60
+ handle: ConfigKeyHandle<T>,
61
+ ): Promise<ConfigValue<T> | undefined>;
62
+ async function configAccessor(
63
+ keyOrHandle: string | ConfigKeyHandle<ConfigKeyType>,
64
+ ): Promise<string | number | boolean | undefined> {
65
+ const qualifiedKey = typeof keyOrHandle === "string" ? keyOrHandle : keyOrHandle.name;
66
+ const keyDef = registry.getConfigKey(qualifiedKey);
67
+ if (!keyDef) return undefined;
68
+ return resolver.get(qualifiedKey, keyDef, tenantId, userId, db);
69
+ }
70
+ return configAccessor;
71
+ }
72
+
73
+ // Pass to the test-stack / server-boot as `_configAccessorFactory` —
74
+ // `buildHandlerContext` mints a per-user `ctx.config` from this.
75
+ export function createConfigAccessorFactory(
76
+ registry: Registry,
77
+ resolver: ConfigResolver,
78
+ ): ConfigAccessorFactory {
79
+ return ({ user, db }) => createConfigAccessor(registry, resolver, user.tenantId, user.id, db);
80
+ }
81
+
82
+ // Single point of truth for "this handler needs the resolver". Throws a
83
+ // proper InternalError (with i18n) instead of bare Error, and points the
84
+ // caller at the boot wiring step that's missing — so a future debug
85
+ // session reads "config feature not wired into AppContext" instead of a
86
+ // generic "configResolver missing".
87
+ export function requireConfigResolver(ctx: HandlerContext, handlerName: string): ConfigResolver {
88
+ if (!ctx.configResolver) {
89
+ throw new InternalError({
90
+ message:
91
+ `[${handlerName}] ctx.configResolver missing — pass createConfigAccessorFactory's ` +
92
+ `output via extraContext._configAccessorFactory and the resolver via ` +
93
+ `extraContext.configResolver at boot.`,
94
+ });
95
+ }
96
+ return ctx.configResolver;
97
+ }
98
+
99
+ // Mirror of requireConfigResolver for the encryption round-trip side.
100
+ // Only keys declared `encrypted: true` need this — the setter calls it
101
+ // lazily so apps that never wire encryption still boot (and only crash
102
+ // if a handler tries to write an encrypted key without the provider in
103
+ // place, pointing at the exact wiring gap).
104
+ export function requireConfigEncryption(
105
+ ctx: HandlerContext,
106
+ handlerName: string,
107
+ ): EncryptionProvider {
108
+ if (!ctx.configEncryption) {
109
+ throw new InternalError({
110
+ message:
111
+ `[${handlerName}] ctx.configEncryption missing — at least one config key declares ` +
112
+ `encrypted: true, so the boot wiring must pass an EncryptionProvider via ` +
113
+ `extraContext.configEncryption (same instance the resolver was built with).`,
114
+ });
115
+ }
116
+ return ctx.configEncryption;
117
+ }
@@ -0,0 +1,209 @@
1
+ import {
2
+ access,
3
+ type ConfigKeyDefinition,
4
+ ConfigScopes,
5
+ createSystemConfig,
6
+ createTenantConfig,
7
+ createUserConfig,
8
+ type Registry,
9
+ type SessionUser,
10
+ SYSTEM_ROLE,
11
+ SYSTEM_TENANT_ID,
12
+ type TenantId,
13
+ } from "@cosmicdrift/kumiko-framework/engine";
14
+ import { describe, expect, test } from "vitest";
15
+ import { prepareConfigWrite, validateBounds } from "../../write-helpers";
16
+
17
+ // Minimal Registry stub — only getConfigKey is exercised by prepareConfigWrite.
18
+ function registryStub(keys: Record<string, unknown>): Registry {
19
+ return {
20
+ getConfigKey: (name: string) => keys[name] as never,
21
+ // biome-ignore lint/suspicious/noExplicitAny: the other Registry methods aren't touched by prepareConfigWrite — cast documents the intent.
22
+ } as any;
23
+ }
24
+
25
+ // Minimal user shape — prepareConfigWrite reads roles / tenantId / id.
26
+ function userStub(
27
+ roles: readonly string[],
28
+ tenantId = "tenant-1" as TenantId,
29
+ id = "user-1",
30
+ ): SessionUser {
31
+ return { id, tenantId, roles } as SessionUser;
32
+ }
33
+
34
+ // Built via the public factory (same path a feature-dev takes in r.config).
35
+ // Kept const so a test that asserts `result.keyDef === TENANT_KEY_DEF`
36
+ // actually compares to the stable reference.
37
+ const TENANT_KEY_DEF = createTenantConfig("text", { write: access.roles("Admin") });
38
+
39
+ describe("prepareConfigWrite", () => {
40
+ test("returns NotFound failure when the key is not registered", () => {
41
+ const result = prepareConfigWrite({
42
+ registry: registryStub({}),
43
+ user: userStub(["Admin"]),
44
+ key: "unknown:key",
45
+ });
46
+ expect(result.ok).toBe(false);
47
+ if (result.ok) throw new Error("unreachable");
48
+ expect(result.failure.isSuccess).toBe(false);
49
+ expect(result.failure.error.code).toBe("not_found");
50
+ expect(result.failure.error.i18nKey).toBe("config.errors.unknownKey");
51
+ });
52
+
53
+ test("returns AccessDenied when the user's roles do not include the key's write roles", () => {
54
+ const result = prepareConfigWrite({
55
+ registry: registryStub({ "ns:config:foo": TENANT_KEY_DEF }),
56
+ user: userStub(["ReadOnly"]),
57
+ key: "ns:config:foo",
58
+ });
59
+ expect(result.ok).toBe(false);
60
+ if (result.ok) throw new Error("unreachable");
61
+ expect(result.failure.error.code).toBe("access_denied");
62
+ });
63
+
64
+ test("returns AccessDenied (system-only) when the key's write access is SYSTEM_ROLE", () => {
65
+ const systemKey = createTenantConfig("text", { write: access.system });
66
+ const result = prepareConfigWrite({
67
+ registry: registryStub({ "ns:config:secret": systemKey }),
68
+ user: userStub(["SystemAdmin"]),
69
+ key: "ns:config:secret",
70
+ });
71
+ expect(result.ok).toBe(false);
72
+ if (result.ok) throw new Error("unreachable");
73
+ expect(result.failure.error.i18nKey).toBe("config.errors.systemOnly");
74
+ });
75
+
76
+ test("system-only key is writable by a caller that actually carries SYSTEM_ROLE", () => {
77
+ // Post-ES escape hatch: out-of-band writes (jobs, seeds, framework-
78
+ // internal flows) used to bypass checkWriteAccess via resolver.set.
79
+ // The handler is the only write path now, so SYSTEM_ROLE must flow
80
+ // through — otherwise billing/quota/session-cleanup jobs can't touch
81
+ // system-only config anymore. Non-system roles stay blocked by the
82
+ // test above.
83
+ const systemKey = createTenantConfig("text", { write: access.system });
84
+ const result = prepareConfigWrite({
85
+ registry: registryStub({ "ns:config:secret": systemKey }),
86
+ user: userStub([SYSTEM_ROLE]),
87
+ key: "ns:config:secret",
88
+ });
89
+ expect(result.ok).toBe(true);
90
+ if (!result.ok) throw new Error("unreachable");
91
+ expect(result.keyDef).toBe(systemKey);
92
+ });
93
+
94
+ test("system-only key stays blocked for Admin even when they also carry other roles", () => {
95
+ // Regression guard: don't let "Admin + Billing" accidentally pass the
96
+ // system-only gate because role aggregation loosens the check.
97
+ const systemKey = createTenantConfig("text", { write: access.system });
98
+ const result = prepareConfigWrite({
99
+ registry: registryStub({ "ns:config:secret": systemKey }),
100
+ user: userStub(["Admin", "Billing"]),
101
+ key: "ns:config:secret",
102
+ });
103
+ expect(result.ok).toBe(false);
104
+ if (result.ok) throw new Error("unreachable");
105
+ expect(result.failure.error.i18nKey).toBe("config.errors.systemOnly");
106
+ });
107
+
108
+ test("ok-path falls back to the key's declared scope when no scope is passed", () => {
109
+ const result = prepareConfigWrite({
110
+ registry: registryStub({ "ns:config:foo": TENANT_KEY_DEF }),
111
+ user: userStub(["Admin"]),
112
+ key: "ns:config:foo",
113
+ });
114
+ expect(result.ok).toBe(true);
115
+ if (!result.ok) throw new Error("unreachable");
116
+ expect(result.scope).toBe(ConfigScopes.tenant);
117
+ expect(result.tenantId).toBe("tenant-1");
118
+ expect(result.userId).toBeNull();
119
+ expect(result.keyDef).toBe(TENANT_KEY_DEF);
120
+ });
121
+
122
+ test("ok-path: scope=system maps tenantId to SYSTEM_TENANT_ID, userId to null", () => {
123
+ // Default system-scope write role is "system" (programmatic-only) —
124
+ // override to admin so a SystemAdmin can actually trigger this path.
125
+ // System-scope rows carry the SYSTEM_TENANT_ID sentinel on tenant_id
126
+ // (the projection column is NOT NULL post-ES).
127
+ const systemKey = createSystemConfig("text", { write: access.admin });
128
+ const result = prepareConfigWrite({
129
+ registry: registryStub({ "ns:config:foo": systemKey }),
130
+ user: userStub(["SystemAdmin"]),
131
+ key: "ns:config:foo",
132
+ scope: ConfigScopes.system,
133
+ });
134
+ expect(result.ok).toBe(true);
135
+ if (!result.ok) throw new Error("unreachable");
136
+ expect(result.tenantId).toBe(SYSTEM_TENANT_ID);
137
+ expect(result.userId).toBeNull();
138
+ });
139
+
140
+ test("ok-path: scope=user maps both tenantId and userId from the caller", () => {
141
+ const userKey = createUserConfig("text");
142
+ const result = prepareConfigWrite({
143
+ registry: registryStub({ "ns:config:foo": userKey }),
144
+ user: userStub(["Admin"], "t-99" as TenantId, "u-99"),
145
+ key: "ns:config:foo",
146
+ scope: ConfigScopes.user,
147
+ });
148
+ expect(result.ok).toBe(true);
149
+ if (!result.ok) throw new Error("unreachable");
150
+ expect(result.tenantId).toBe("t-99");
151
+ expect(result.userId).toBe("u-99");
152
+ });
153
+ });
154
+
155
+ describe("validateBounds", () => {
156
+ const numberKey = createTenantConfig("number", {
157
+ default: 10,
158
+ bounds: { min: 1, max: 100 },
159
+ });
160
+
161
+ test("returns null for value inside bounds", () => {
162
+ expect(validateBounds(50, numberKey)).toBeNull();
163
+ expect(validateBounds(1, numberKey)).toBeNull(); // boundary min
164
+ expect(validateBounds(100, numberKey)).toBeNull(); // boundary max
165
+ });
166
+
167
+ test("returns out_of_bounds error when value is below min", () => {
168
+ const err = validateBounds(0, numberKey);
169
+ expect(err).not.toBeNull();
170
+ expect(err?.code).toBe("validation_error");
171
+ const details = err?.details as { fields: Array<{ code: string; params: unknown }> };
172
+ expect(details.fields[0]?.code).toBe("out_of_bounds");
173
+ expect(details.fields[0]?.params).toMatchObject({ value: 0, min: 1, max: 100 });
174
+ });
175
+
176
+ test("returns out_of_bounds error when value is above max", () => {
177
+ const err = validateBounds(101, numberKey);
178
+ expect(err).not.toBeNull();
179
+ const details = err?.details as { fields: Array<{ params: unknown }> };
180
+ expect(details.fields[0]?.params).toMatchObject({ value: 101, min: 1, max: 100 });
181
+ });
182
+
183
+ test("returns null when bounds declared with only min (no max)", () => {
184
+ // Spread on a factory-produced def is the idiomatic way to tweak a
185
+ // single field without re-stating the whole declaration.
186
+ const minOnly: ConfigKeyDefinition = { ...numberKey, bounds: { min: 1 } };
187
+ expect(validateBounds(9999, minOnly)).toBeNull();
188
+ expect(validateBounds(0, minOnly)).not.toBeNull();
189
+ });
190
+
191
+ test("returns null when bounds declared with only max (no min)", () => {
192
+ const maxOnly: ConfigKeyDefinition = { ...numberKey, bounds: { max: 100 } };
193
+ expect(validateBounds(-9999, maxOnly)).toBeNull();
194
+ expect(validateBounds(101, maxOnly)).not.toBeNull();
195
+ });
196
+
197
+ test("returns null for keys without bounds declared (unrestricted)", () => {
198
+ const { bounds: _bounds, ...unrestricted } = numberKey;
199
+ expect(validateBounds(99999, unrestricted)).toBeNull();
200
+ expect(validateBounds(-99999, unrestricted)).toBeNull();
201
+ });
202
+
203
+ test("returns null for non-number key types (bounds only applies to number)", () => {
204
+ const textKey = createTenantConfig("text");
205
+ // Even if bounds were somehow present on a text key, non-number values
206
+ // are unreachable here — validateType runs first and rejects them.
207
+ expect(validateBounds("any", textKey)).toBeNull();
208
+ });
209
+ });
@@ -0,0 +1,45 @@
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
+ import { ConfigScopes, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { z } from "zod";
4
+ import { configValueEntity, configValuesTable } from "../table";
5
+ import { findConfigRow, prepareConfigWrite } from "../write-helpers";
6
+
7
+ const scopeEnum = z.enum([ConfigScopes.system, ConfigScopes.tenant, ConfigScopes.user]);
8
+
9
+ const executor = createEventStoreExecutor(configValuesTable, configValueEntity, {
10
+ entityName: "config-value",
11
+ });
12
+
13
+ export const resetWrite = defineWriteHandler({
14
+ name: "reset",
15
+ schema: z.object({
16
+ key: z.string(),
17
+ scope: scopeEnum.optional(),
18
+ }),
19
+ // Per-key access enforcement lives inside the handler via checkWriteAccess.
20
+ access: { openToAll: true },
21
+ handler: async (event, ctx) => {
22
+ const db = ctx.db;
23
+
24
+ const prep = prepareConfigWrite({
25
+ registry: ctx.registry,
26
+ user: event.user,
27
+ key: event.payload.key,
28
+ scope: event.payload.scope,
29
+ });
30
+ if (!prep.ok) return prep.failure;
31
+ const { scope, tenantId, userId } = prep;
32
+
33
+ const existing = await findConfigRow(db, event.payload.key, tenantId, userId);
34
+
35
+ // No-op when there is nothing to reset. Pre-ES this path silently did
36
+ // nothing too — keep the contract intact so callers can reset
37
+ // idempotently without a 404 dance.
38
+ if (existing) {
39
+ const result = await executor.delete({ id: existing.id }, event.user, db);
40
+ if (!result.isSuccess) return result;
41
+ }
42
+
43
+ return { isSuccess: true, data: { key: event.payload.key, scope } };
44
+ },
45
+ });
@@ -0,0 +1,22 @@
1
+ import { type ConfigKeyDefinition, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { z } from "zod";
3
+ import { hasConfigAccess } from "../write-helpers";
4
+
5
+ export const schemaQuery = defineQueryHandler({
6
+ name: "schema",
7
+ schema: z.object({}),
8
+ // Per-key read access enforced via hasConfigAccess inside the handler.
9
+ access: { openToAll: true },
10
+ handler: async (query, ctx) => {
11
+ const registry = ctx.registry;
12
+ const allKeys = registry.getAllConfigKeys();
13
+ const result: Record<string, ConfigKeyDefinition> = {};
14
+
15
+ for (const [qualifiedKey, keyDef] of allKeys) {
16
+ if (!hasConfigAccess(keyDef.access.read, query.user.roles)) continue;
17
+ result[qualifiedKey] = keyDef;
18
+ }
19
+
20
+ return result;
21
+ },
22
+ });
@@ -0,0 +1,93 @@
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
+ import { ConfigScopes, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { z } from "zod";
5
+ import { requireConfigEncryption } from "../feature";
6
+ import { configValueEntity, configValuesTable } from "../table";
7
+ import {
8
+ findConfigRow,
9
+ prepareConfigWrite,
10
+ validateBounds,
11
+ validateScope,
12
+ validateType,
13
+ } from "../write-helpers";
14
+
15
+ const scopeEnum = z.enum([ConfigScopes.system, ConfigScopes.tenant, ConfigScopes.user]);
16
+
17
+ const executor = createEventStoreExecutor(configValuesTable, configValueEntity, {
18
+ entityName: "config-value",
19
+ });
20
+
21
+ export const setWrite = defineWriteHandler({
22
+ name: "set",
23
+ schema: z.object({
24
+ key: z.string(),
25
+ value: z.union([z.string(), z.number(), z.boolean()]),
26
+ scope: scopeEnum.optional(),
27
+ }),
28
+ // Per-key access enforcement lives inside the handler via checkWriteAccess.
29
+ access: { openToAll: true },
30
+ handler: async (event, ctx) => {
31
+ const db = ctx.db;
32
+
33
+ const prep = prepareConfigWrite({
34
+ registry: ctx.registry,
35
+ user: event.user,
36
+ key: event.payload.key,
37
+ scope: event.payload.scope,
38
+ });
39
+ if (!prep.ok) return prep.failure;
40
+ const { keyDef, scope, tenantId, userId } = prep;
41
+
42
+ const scopeError = validateScope(scope, keyDef.scope, event.payload.key);
43
+ if (scopeError) return writeFailure(scopeError);
44
+
45
+ const typeError = validateType(event.payload.value, keyDef);
46
+ if (typeError) return writeFailure(typeError);
47
+
48
+ // Bounds enforcement: hard-reject (not silent-clamp). A caller that
49
+ // sends 9999 for a bounds.max=1000 key should see a 422 and fix their
50
+ // input — silent clamping would make `get` return a different value
51
+ // than what was sent, which is a UX trap with no upside.
52
+ const boundsError = validateBounds(event.payload.value, keyDef);
53
+ if (boundsError) return writeFailure(boundsError);
54
+
55
+ let serialized = JSON.stringify(event.payload.value);
56
+ if (keyDef.encrypted) {
57
+ const encryption = requireConfigEncryption(ctx, "config:write:set");
58
+ serialized = encryption.encrypt(serialized);
59
+ }
60
+
61
+ const existing = await findConfigRow(db, event.payload.key, tenantId, userId);
62
+
63
+ if (existing) {
64
+ const result = await executor.update(
65
+ {
66
+ id: existing.id,
67
+ version: existing.version,
68
+ changes: { value: serialized },
69
+ },
70
+ event.user,
71
+ db,
72
+ );
73
+ if (!result.isSuccess) return result;
74
+ } else {
75
+ const result = await executor.create(
76
+ {
77
+ key: event.payload.key,
78
+ value: serialized,
79
+ tenantId,
80
+ userId,
81
+ },
82
+ event.user,
83
+ db,
84
+ );
85
+ if (!result.isSuccess) return result;
86
+ }
87
+
88
+ return {
89
+ isSuccess: true,
90
+ data: { key: event.payload.key, value: event.payload.value, scope },
91
+ };
92
+ },
93
+ });
@@ -0,0 +1,43 @@
1
+ import { type ConfigScope, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { z } from "zod";
3
+ import { requireConfigResolver } from "../feature";
4
+ import { deserializeValue } from "../resolver";
5
+ import { hasConfigAccess } from "../write-helpers";
6
+
7
+ export const valuesQuery = defineQueryHandler({
8
+ name: "values",
9
+ schema: z.object({}),
10
+ // Per-key read access enforced via hasConfigAccess inside the handler.
11
+ access: { openToAll: true },
12
+ handler: async (query, ctx) => {
13
+ const db = ctx.db;
14
+ const registry = ctx.registry;
15
+ const resolver = requireConfigResolver(ctx, "config:query:values");
16
+
17
+ const allKeys = registry.getAllConfigKeys();
18
+ const storedValues = await resolver.getAll(query.user.tenantId, query.user.id, db);
19
+
20
+ const result: Record<
21
+ string,
22
+ { value: string | number | boolean | undefined; scope: ConfigScope }
23
+ > = {};
24
+
25
+ for (const [qualifiedKey, keyDef] of allKeys) {
26
+ if (!hasConfigAccess(keyDef.access.read, query.user.roles)) continue;
27
+
28
+ const stored = storedValues.get(qualifiedKey);
29
+ let value: string | number | boolean | undefined;
30
+ if (keyDef.encrypted) {
31
+ value = stored ? "••••••" : undefined;
32
+ } else if (stored?.value !== null && stored?.value !== undefined) {
33
+ value = deserializeValue(stored.value, keyDef.type);
34
+ } else {
35
+ value = keyDef.default;
36
+ }
37
+
38
+ result[qualifiedKey] = { value, scope: keyDef.scope };
39
+ }
40
+
41
+ return result;
42
+ },
43
+ });
@@ -0,0 +1,15 @@
1
+ export {
2
+ CONFIG_FEATURE,
3
+ ConfigErrors,
4
+ ConfigHandlers,
5
+ ConfigQueries,
6
+ } from "./constants";
7
+ export type { ConfigContext } from "./feature";
8
+ export {
9
+ createConfigAccessor,
10
+ createConfigAccessorFactory,
11
+ createConfigFeature,
12
+ } from "./feature";
13
+ export type { AppConfigOverrides, ConfigResolver } from "./resolver";
14
+ export { createConfigResolver, validateAppOverrides } from "./resolver";
15
+ export { configValuesTable } from "./table";