@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,20 @@
1
+ // Fully-qualified event name for feature-toggle changes. Kept as a constant
2
+ // so write-handler + tests reference one source.
3
+ export const FEATURE_TOGGLE_SET_EVENT_NAME = "feature-toggles:event:toggle-set";
4
+
5
+ // Aggregate type for toggle-set events. Shares the feature name to keep the
6
+ // events-table grep-friendly: every row belonging to this feature carries
7
+ // "feature-toggles" in its aggregate_type column.
8
+ export const FEATURE_TOGGLE_AGGREGATE_TYPE = "feature-toggles";
9
+
10
+ // Error reasons surfaced from feature-toggle handlers. Scoped to the
11
+ // feature's namespace per the framework's reason-convention.
12
+ export const FeatureToggleErrors = {
13
+ // set-handler attempted to toggle a feature that didn't declare
14
+ // r.toggleable(). The dispatcher's gate ignores such features anyway,
15
+ // but writing a row for them would create the illusion of configurability.
16
+ notToggleable: "feature_not_toggleable",
17
+ // set-handler attempted to toggle a feature name that isn't registered.
18
+ // Prevents typos from silently piling up orphan rows.
19
+ unknownFeature: "unknown_feature",
20
+ } as const;
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+
3
+ // Toggle-change event payload.
4
+ //
5
+ // Contract: every set-operation produces exactly one event, even when
6
+ // enabled === previousEnabled. Redundant writes are legal (confirms the
7
+ // current state, useful for ops "make sure feature X is on"), so consumers
8
+ // that filter for actual transitions must compare enabled !== previousEnabled
9
+ // themselves. `previousEnabled` is null when this is the first time the
10
+ // feature is being toggled (no row existed).
11
+ export const featureToggleSetSchema = z.object({
12
+ featureName: z.string().min(1),
13
+ enabled: z.boolean(),
14
+ previousEnabled: z.boolean().nullable(),
15
+ updatedBy: z.string(),
16
+ });
17
+
18
+ export type FeatureToggleSetPayload = z.infer<typeof featureToggleSetSchema>;
@@ -0,0 +1,98 @@
1
+ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { FEATURE_TOGGLE_SET_EVENT_NAME } from "./constants";
3
+ import { featureToggleSetSchema } from "./events";
4
+ import { listQuery } from "./handlers/list.query";
5
+ import { registeredQuery } from "./handlers/registered.query";
6
+ import { createSetWriteHandler } from "./handlers/set.write";
7
+ import type { GlobalFeatureToggleRuntime } from "./toggle-runtime";
8
+
9
+ // IMPORTANT: feature-toggles itself is NOT r.toggleable. Making it
10
+ // toggleable would brick the system — once disabled, no handler of this
11
+ // feature is reachable to turn it back on. The boot-validator won't catch
12
+ // this (it only warns about dependency shapes), so the guarantee lives in
13
+ // this file: do not add r.toggleable() here.
14
+
15
+ export type FeatureTogglesOptions = {
16
+ // Accessor for the in-memory snapshot the dispatcher gate reads. Must
17
+ // return a resolved runtime by the time the feature's set-handler is
18
+ // called — NOT by the time the feature is registered. This matters
19
+ // because createFeatureToggleRuntime needs the registry that
20
+ // setupTestStack / buildServer builds from the feature list, so the
21
+ // runtime and the feature are chicken-and-egg at wire-up time. Passing
22
+ // an accessor (vs the runtime directly) lets the caller close over a
23
+ // mutable holder.
24
+ //
25
+ // Production setup: resolve the runtime after buildServer returns, then
26
+ // pass `() => runtime`. For tests, use createLateBoundHolder + .get().
27
+ readonly getRuntime: () => GlobalFeatureToggleRuntime;
28
+ };
29
+
30
+ export function createFeatureTogglesFeature(options: FeatureTogglesOptions): FeatureDefinition {
31
+ return defineFeature("feature-toggles", (r) => {
32
+ r.systemScope();
33
+
34
+ // Toggle-change domain event. The event ends up in the events-table
35
+ // alongside every other write — audit.list picks it up automatically,
36
+ // no dedicated projection needed. Qualified name after prefixing:
37
+ // "feature-toggles:event:toggle-set" (see constants.FEATURE_TOGGLE_SET_EVENT_NAME).
38
+ r.defineEvent("toggle-set", featureToggleSetSchema);
39
+
40
+ const handlers = {
41
+ set: r.writeHandler(createSetWriteHandler(options.getRuntime)),
42
+ };
43
+
44
+ const queries = {
45
+ list: r.queryHandler(listQuery),
46
+ registered: r.queryHandler(registeredQuery),
47
+ };
48
+
49
+ // toggle-cache-sync — multi-instance snapshot propagation. Every
50
+ // API/worker instance runs its own dispatcher cursor on this MSP
51
+ // (delivery: "per-instance") and converges its in-memory snapshot on
52
+ // every toggle-set event it observes. Named "cache-sync" (not
53
+ // "projection" or "audit") because it's side-effect-only
54
+ // infrastructure — the framework's boot-validator also rejects
55
+ // per-instance MSPs that carry a `table`.
56
+ //
57
+ // Why this is correct alongside the set-handler's own `runtime.apply`:
58
+ // - local apply = immediate response-latency optimization so the
59
+ // next request on the same instance sees the flip without a
60
+ // dispatcher-tick round-trip
61
+ // - MSP = multi-instance propagation + crash-recovery. If a process
62
+ // crashes between appendEvent (persisted) and the local apply
63
+ // (volatile), the MSP rebuilds the snapshot on restart; if
64
+ // instance B never ran the write, the MSP is how it learns. Both
65
+ // paths are idempotent — apply is Map.set, replay on boot just
66
+ // converges to the DB state that initialize() already loaded.
67
+ //
68
+ // Requires: options.getRuntime() must resolve by the time the
69
+ // dispatcher processes its first toggle-set event. The holder-based
70
+ // wire-up (see FeatureTogglesOptions.getRuntime docstring) guarantees
71
+ // this in setupTestStack and production boot.
72
+ r.multiStreamProjection({
73
+ name: "toggle-cache-sync",
74
+ delivery: "per-instance",
75
+ apply: {
76
+ [FEATURE_TOGGLE_SET_EVENT_NAME]: async (event) => {
77
+ // The event payload shape is guaranteed by featureToggleSetSchema
78
+ // (validated on append). Shallow-cast to a typed shape rather
79
+ // than re-parsing — the payload round-trips through JSON and is
80
+ // fixed at the source.
81
+ const payload = event.payload as { featureName: string; enabled: boolean };
82
+ options.getRuntime().apply(payload.featureName, payload.enabled);
83
+ },
84
+ },
85
+ });
86
+
87
+ return { handlers, queries };
88
+ });
89
+ }
90
+
91
+ export { FEATURE_TOGGLE_SET_EVENT_NAME, FeatureToggleErrors } from "./constants";
92
+ export { globalFeatureStateTable } from "./global-feature-state-table";
93
+ // Re-export the runtime factory + class so app-boot code has a single
94
+ // import path: "@cosmicdrift/kumiko-bundled-features/feature-toggles".
95
+ export {
96
+ createFeatureToggleRuntime,
97
+ GlobalFeatureToggleRuntime,
98
+ } from "./toggle-runtime";
@@ -0,0 +1,28 @@
1
+ import {
2
+ boolean,
3
+ instant,
4
+ integer,
5
+ table as pgTable,
6
+ text,
7
+ } from "@cosmicdrift/kumiko-framework/db";
8
+ import { sql } from "drizzle-orm";
9
+
10
+ // Global feature-toggle override state. One row per feature that has ever
11
+ // been explicitly flipped by an operator. Missing row = "no override,
12
+ // fall back to the feature's r.toggleable({ default }) value".
13
+ //
14
+ // PK is featureName (text) — not a surrogate UUID — because the feature
15
+ // name IS the identity here. No tenantId: this is a global override that
16
+ // applies across every tenant (per-tenant toggles are intentionally out of
17
+ // scope, see core-feature-toggles.md).
18
+ export const globalFeatureStateTable = pgTable("read_global_feature_state", {
19
+ featureName: text("feature_name").primaryKey(),
20
+ enabled: boolean("enabled").notNull(),
21
+ // Optimistic-lock column. The set-handler reads the existing row, then
22
+ // updates with `WHERE feature_name = ? AND version = ?`; a 0-row update
23
+ // means someone else wrote concurrently — the handler retries the fetch.
24
+ version: integer("version").notNull().default(1),
25
+ updatedAt: instant("updated_at").default(sql`now()`).notNull(),
26
+ // UserId (text — SessionUser.id is a uuid string post-ES).
27
+ updatedBy: text("updated_by"),
28
+ });
@@ -0,0 +1,26 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { z } from "zod";
3
+ import { globalFeatureStateTable } from "../global-feature-state-table";
4
+
5
+ // List every row in the global_feature_state table — i.e. every feature
6
+ // that has ever been explicitly flipped. Features without a row aren't
7
+ // returned; callers must combine this with `registered` to see the full
8
+ // effective state (registered features + their current override, if any).
9
+ export const listQuery = defineQueryHandler({
10
+ name: "list",
11
+ schema: z.object({}),
12
+ access: { roles: ["SystemAdmin", "Admin"] },
13
+ handler: async (_event, ctx) => {
14
+ type Row = typeof globalFeatureStateTable.$inferSelect;
15
+ const rows = (await ctx.db.select().from(globalFeatureStateTable)) as Row[];
16
+ return {
17
+ items: rows.map((r) => ({
18
+ featureName: r.featureName,
19
+ enabled: r.enabled,
20
+ version: r.version,
21
+ updatedAt: r.updatedAt.toString(),
22
+ updatedBy: r.updatedBy,
23
+ })),
24
+ };
25
+ },
26
+ });
@@ -0,0 +1,56 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { z } from "zod";
3
+ import { globalFeatureStateTable } from "../global-feature-state-table";
4
+
5
+ // Inventory of every registered feature, annotated with toggle metadata
6
+ // and the current effective state. This is the canonical "what's here,
7
+ // what's on, what depends on what" snapshot — the UI for the operator
8
+ // toggle screen binds to it.
9
+ //
10
+ // Design: registry introspection (toggleable/default/requires) + a single
11
+ // DB read of overrides. No per-feature DB calls. Scales to however many
12
+ // features an app registers — currently tens, never thousands.
13
+ export const registeredQuery = defineQueryHandler({
14
+ name: "registered",
15
+ schema: z.object({}),
16
+ access: { roles: ["SystemAdmin", "Admin"] },
17
+ handler: async (_event, ctx) => {
18
+ type OverrideRow = Pick<typeof globalFeatureStateTable.$inferSelect, "featureName" | "enabled">;
19
+ const overrideRows = (await ctx.db
20
+ .select({
21
+ featureName: globalFeatureStateTable.featureName,
22
+ enabled: globalFeatureStateTable.enabled,
23
+ })
24
+ .from(globalFeatureStateTable)) as OverrideRow[];
25
+ const overrides = new Map(overrideRows.map((r) => [r.featureName, r.enabled]));
26
+
27
+ const effective = ctx.effectiveFeatures?.();
28
+
29
+ const items = [];
30
+ for (const feature of ctx.registry.features.values()) {
31
+ const toggleable = feature.toggleableDefault !== undefined;
32
+ const override = overrides.get(feature.name);
33
+ items.push({
34
+ name: feature.name,
35
+ toggleable,
36
+ // `default` is null when non-toggleable; the UI must render
37
+ // non-toggleable features as "always on" without an enable/disable
38
+ // control (flipping them would be rejected by the set-handler).
39
+ default: feature.toggleableDefault ?? null,
40
+ // `override` is null when no explicit row exists. That's distinct
41
+ // from "override says on" or "override says off" so the UI can show
42
+ // an "inherits default" indicator.
43
+ override: override ?? null,
44
+ requires: feature.requires,
45
+ // Effective = what the dispatcher-gate actually uses right now,
46
+ // after cascade. When the feature-toggles runtime isn't wired
47
+ // (dev setup without the feature loaded), we surface null so the
48
+ // UI knows the runtime isn't available rather than defaulting
49
+ // to "everything on".
50
+ effective: effective ? effective.has(feature.name) : null,
51
+ });
52
+ }
53
+
54
+ return { items };
55
+ },
56
+ });
@@ -0,0 +1,158 @@
1
+ import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
2
+
3
+ import {
4
+ UnprocessableError,
5
+ VersionConflictError,
6
+ writeFailure,
7
+ } from "@cosmicdrift/kumiko-framework/errors";
8
+ import { and, eq, sql } from "drizzle-orm";
9
+ import { Temporal } from "temporal-polyfill";
10
+ import { z } from "zod";
11
+ import {
12
+ FEATURE_TOGGLE_AGGREGATE_TYPE,
13
+ FEATURE_TOGGLE_SET_EVENT_NAME,
14
+ FeatureToggleErrors,
15
+ } from "../constants";
16
+ import { globalFeatureStateTable } from "../global-feature-state-table";
17
+ import type { GlobalFeatureToggleRuntime } from "../toggle-runtime";
18
+
19
+ // Factory: binds a runtime accessor to the handler at registration time.
20
+ // The runtime holds the in-memory snapshot that the dispatcher's gate
21
+ // reads; every successful set() call must update it, otherwise the flip
22
+ // won't take effect until the next boot.
23
+ //
24
+ // Accessor form (instead of direct runtime ref) supports the bootstrapping
25
+ // flow: tests + setupTestStack construct the feature definition BEFORE the
26
+ // runtime exists (the runtime needs the registry, which setupTestStack
27
+ // builds from the features). The accessor is resolved lazily, at call time.
28
+ export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRuntime) {
29
+ return defineWriteHandler({
30
+ name: "set",
31
+ schema: z.object({
32
+ featureName: z.string().min(1),
33
+ enabled: z.boolean(),
34
+ }),
35
+ // Platform-operator action — SystemAdmin only.
36
+ access: { roles: ["SystemAdmin"] },
37
+ handler: async (event, ctx) => {
38
+ const { featureName, enabled } = event.payload;
39
+
40
+ // Guard 1: featureName must be a registered feature. Otherwise we'd
41
+ // pile up orphan rows from typos that the gate would silently apply
42
+ // (if someone ever added a feature with that name later).
43
+ const feature = ctx.registry.getFeature(featureName);
44
+ if (!feature) {
45
+ return writeFailure(
46
+ new UnprocessableError(FeatureToggleErrors.unknownFeature, {
47
+ i18nKey: "feature-toggles.errors.unknownFeature",
48
+ details: { featureName },
49
+ }),
50
+ );
51
+ }
52
+
53
+ // Guard 2: feature must be toggleable. Non-toggleable features (auth,
54
+ // tenant, user, feature-toggles itself) must stay on — the gate
55
+ // would ignore any row, but writing one sends the wrong signal to
56
+ // anyone reading the table.
57
+ if (feature.toggleableDefault === undefined) {
58
+ return writeFailure(
59
+ new UnprocessableError(FeatureToggleErrors.notToggleable, {
60
+ i18nKey: "feature-toggles.errors.notToggleable",
61
+ details: { featureName },
62
+ }),
63
+ );
64
+ }
65
+
66
+ // Read current state for event payload + optimistic-lock version.
67
+ // `$inferSelect` narrows the result shape to the real table schema —
68
+ // no hand-rolled cast, no drift if a column is added later.
69
+ type StateRow = typeof globalFeatureStateTable.$inferSelect;
70
+ const [existing] = (await ctx.db
71
+ .select()
72
+ .from(globalFeatureStateTable)
73
+ .where(eq(globalFeatureStateTable.featureName, featureName))
74
+ .limit(1)) as StateRow[];
75
+
76
+ const previousEnabled = existing?.enabled ?? null;
77
+
78
+ if (!existing) {
79
+ // First-time override: insert.
80
+ await ctx.db.insert(globalFeatureStateTable).values({
81
+ featureName,
82
+ enabled,
83
+ version: 1,
84
+ updatedBy: event.user.id,
85
+ updatedAt: Temporal.Now.instant(),
86
+ });
87
+ } else {
88
+ // Upsert with optimistic lock. Two operators flipping the same
89
+ // toggle simultaneously is rare but possible — the version-WHERE
90
+ // ensures only one wins; the loser sees VersionConflictError.
91
+ const updated = await ctx.db
92
+ .update(globalFeatureStateTable)
93
+ .set({
94
+ enabled,
95
+ version: sql<number>`${globalFeatureStateTable.version} + 1`,
96
+ updatedBy: event.user.id,
97
+ updatedAt: Temporal.Now.instant(),
98
+ })
99
+ .where(
100
+ and(
101
+ eq(globalFeatureStateTable.featureName, featureName),
102
+ eq(globalFeatureStateTable.version, existing.version),
103
+ ),
104
+ )
105
+ .returning();
106
+
107
+ if (updated.length === 0) {
108
+ return writeFailure(
109
+ new VersionConflictError({
110
+ entityId: featureName,
111
+ expectedVersion: existing.version,
112
+ currentVersion: existing.version + 1,
113
+ }),
114
+ );
115
+ }
116
+ }
117
+
118
+ // Domain event — the event-store IS the toggle-change audit trail.
119
+ // aggregateId = SYSTEM_TENANT_ID (uuid) because the events table
120
+ // types aggregate_id as uuid. Per-feature stream isolation would
121
+ // need synthetic UUIDs from the feature-name, which add nothing
122
+ // audit-wise; one shared toggle-changes stream per system is fine,
123
+ // and filtering by payload.featureName is trivial at query time.
124
+ // This mirrors how `config` handles the same constraint for
125
+ // its config-changed events.
126
+ // appendEventUnsafe — bundled-features ohne lokalen Wrapper. Apps
127
+ // mit `yarn kumiko codegen` kriegen `.kumiko/define.ts` als strict-
128
+ // path; bundled-features bleibt bei der unsafe-Variante. Schema-
129
+ // Validation läuft trotzdem via r.defineEvent("toggle-set", ...).
130
+ await ctx.appendEventUnsafe({
131
+ aggregateId: SYSTEM_TENANT_ID,
132
+ aggregateType: FEATURE_TOGGLE_AGGREGATE_TYPE,
133
+ type: FEATURE_TOGGLE_SET_EVENT_NAME,
134
+ payload: {
135
+ featureName,
136
+ enabled,
137
+ previousEnabled,
138
+ updatedBy: event.user.id,
139
+ },
140
+ });
141
+
142
+ // Update the local in-memory snapshot. Done AFTER the DB write +
143
+ // event append so a crash in either leaves the snapshot consistent
144
+ // with what's persisted. This is the response-latency optimization:
145
+ // the next request on THIS instance sees the flip without waiting
146
+ // for a dispatcher tick. Other instances learn the change through
147
+ // the `toggle-cache-sync` MSP (see feature-toggles-feature.ts). Both
148
+ // paths are idempotent — Map.set is last-write-wins and the DB is
149
+ // the source of truth after boot-time initialize().
150
+ getRuntime().apply(featureName, enabled);
151
+
152
+ return {
153
+ isSuccess: true,
154
+ data: { featureName, enabled, previousEnabled },
155
+ };
156
+ },
157
+ });
158
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ createFeatureToggleRuntime,
3
+ createFeatureTogglesFeature,
4
+ FEATURE_TOGGLE_SET_EVENT_NAME,
5
+ FeatureToggleErrors,
6
+ type FeatureTogglesOptions,
7
+ GlobalFeatureToggleRuntime,
8
+ globalFeatureStateTable,
9
+ } from "./feature";
@@ -0,0 +1,73 @@
1
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import {
3
+ computeEffectiveFeatures,
4
+ type Registry,
5
+ type ToggleReader,
6
+ } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { globalFeatureStateTable } from "./global-feature-state-table";
8
+
9
+ // Holds the current global-override snapshot in memory and exposes a
10
+ // synchronous reader — the dispatcher's feature-gate calls it on every
11
+ // handler invocation, so this must not do I/O on the hot path. The
12
+ // snapshot is loaded once at boot via `.initialize()`, refreshed by the
13
+ // set-handler on the local instance, and kept in sync across
14
+ // instances by the `toggle-cache-sync` MSP (declared on the
15
+ // feature-toggles feature, delivery: "per-instance"). Every API/worker
16
+ // process observes every toggle-set event and applies it to its local
17
+ // snapshot — no Redis / SSE / polling needed; the existing events-table
18
+ // + event-dispatcher pipeline handles propagation.
19
+ export class GlobalFeatureToggleRuntime {
20
+ private snapshot = new Map<string, boolean>();
21
+
22
+ constructor(
23
+ private readonly db: DbConnection,
24
+ private readonly registry: Registry,
25
+ ) {}
26
+
27
+ async initialize(): Promise<void> {
28
+ const rows = await this.db
29
+ .select({
30
+ featureName: globalFeatureStateTable.featureName,
31
+ enabled: globalFeatureStateTable.enabled,
32
+ })
33
+ .from(globalFeatureStateTable);
34
+ this.snapshot = new Map(rows.map((r) => [r.featureName, r.enabled]));
35
+ }
36
+
37
+ // Re-read the full snapshot. Called from the set-handler after a
38
+ // successful write — cheap point-update would be an optimisation but
39
+ // the table is small (O(features)) and this keeps the cache honest in
40
+ // the presence of concurrent external writes (seed scripts, ops SQL).
41
+ async refresh(): Promise<void> {
42
+ await this.initialize();
43
+ }
44
+
45
+ // In-memory cache update. Used by the set-handler when a single
46
+ // featureName transitions — saves a round-trip compared to refresh()
47
+ // while staying correct because set-handlers serialise via optimistic
48
+ // lock. Kept alongside refresh() so both options are explicit.
49
+ apply(featureName: string, enabled: boolean): void {
50
+ this.snapshot.set(featureName, enabled);
51
+ }
52
+
53
+ // The callback shape the dispatcher expects. Computes the effective
54
+ // feature set from the current snapshot + the registry's requires()
55
+ // cascade every call. Cheap: the cascade is a DFS over O(features);
56
+ // for the expected sizes (tens of features per app) this is ~µs.
57
+ effectiveFeatures = (): ReadonlySet<string> => {
58
+ const reader: ToggleReader = (name) => this.snapshot.get(name);
59
+ return computeEffectiveFeatures(this.registry, reader);
60
+ };
61
+ }
62
+
63
+ // Factory for app-boot wiring: instantiate, initialize, return both the
64
+ // runtime (for the set-handler to refresh) and the callback (for
65
+ // createDispatcher's effectiveFeatures option).
66
+ export async function createFeatureToggleRuntime(
67
+ db: DbConnection,
68
+ registry: Registry,
69
+ ): Promise<GlobalFeatureToggleRuntime> {
70
+ const runtime = new GlobalFeatureToggleRuntime(db, registry);
71
+ await runtime.initialize();
72
+ return runtime;
73
+ }
@@ -0,0 +1,35 @@
1
+ // feature.ts contract tests — pin the public surface of the
2
+ // Plugin-API-shaped file-foundation. Provider-specific configs/secrets
3
+ // are tested in their own provider-feature (file-provider-s3/__tests__).
4
+
5
+ import { describe, expect, test } from "vitest";
6
+ import { fileFoundationFeature } from "../feature";
7
+
8
+ describe("fileFoundationFeature — shape", () => {
9
+ test("has the expected name", () => {
10
+ expect(fileFoundationFeature.name).toBe("file-foundation");
11
+ });
12
+
13
+ test("declares config as a hard requirement (provider-selector lives there)", () => {
14
+ expect(fileFoundationFeature.requires).toContain("config");
15
+ });
16
+
17
+ test("does NOT require secrets — provider-plugins own their own secrets", () => {
18
+ expect(fileFoundationFeature.requires).not.toContain("secrets");
19
+ });
20
+ });
21
+
22
+ describe("fileFoundationFeature.exports — typed handles", () => {
23
+ test("exposes only the provider-selector config-key", () => {
24
+ const keys = fileFoundationFeature.exports.configKeys;
25
+ expect(keys.provider).toBeDefined();
26
+ expect((keys as Record<string, unknown>)["bucket"]).toBeUndefined();
27
+ expect((keys as Record<string, unknown>)["region"]).toBeUndefined();
28
+ });
29
+ });
30
+
31
+ describe("fileFoundationFeature — registers extension-point", () => {
32
+ test("declares the 'fileProvider' extension-point", () => {
33
+ expect(fileFoundationFeature.registrarExtensions["fileProvider"]).toBeDefined();
34
+ });
35
+ });