@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,67 @@
1
+ // Shared config-read helpers used by the per-tenant Foundation factories
2
+ // (ai-foundation, mail-foundation, file-foundation). Each foundation reads
3
+ // its config-keys + secret out of `ctx`, narrows undefined → throw, and
4
+ // dispatches to a provider. The narrowing helpers live here as a single
5
+ // source so all three foundations report errors the same way and a fix
6
+ // to one error-message format reaches all callers.
7
+ //
8
+ // **Why a shared module instead of duplicate copies:**
9
+ // First two foundations (ai + mail) hand-rolled identical helpers,
10
+ // the third (file) made it three times. That's the threshold where
11
+ // premature-abstraction-warnings flip to DRY-warnings — three nearly-
12
+ // identical 25-LOC helper-pairs across three files would diverge in
13
+ // error-text under maintenance, exactly the bug-class extraction
14
+ // prevents.
15
+ //
16
+ // **What this module is NOT:**
17
+ // - Not a feature — no `defineFeature`, no boot-time registration.
18
+ // - Not a barrel for everything — only the helpers actually shared
19
+ // across foundations live here. Per-foundation transport/provider-
20
+ // factories stay in their own package.
21
+
22
+ /**
23
+ * Narrow `value | undefined` → `value` with a clear message that names
24
+ * which config key resolved to nothing. Use for keys whose `undefined`
25
+ * means a registry misconfiguration (no value + no default).
26
+ *
27
+ * Foundation-Pattern: this is the wrap-helper around `await ctx.config(
28
+ * featureFoundationFeature.exports.configKeys.someKey)` — the call-site
29
+ * stays a single line per key.
30
+ *
31
+ * **`featureName` is the qualified-name prefix in the error** (e.g.
32
+ * `"ai-foundation"`, `"mail-foundation"`) — included so that an exception
33
+ * surfaced from a multi-foundation app pinpoints the failing foundation.
34
+ */
35
+ export function requireDefined<T>(value: T | undefined, featureName: string, label: string): T {
36
+ if (value === undefined) {
37
+ throw new Error(
38
+ `${featureName}: '${label}' config key resolved to undefined — registry misconfigured (no value + no default)`,
39
+ );
40
+ }
41
+ return value;
42
+ }
43
+
44
+ /**
45
+ * Narrow `string | undefined` → non-empty `string`. Tighter than
46
+ * `requireDefined` for the case where the registry HAS a default (often
47
+ * `""`) but the foundation requires the tenant to have set a real value
48
+ * before the factory can build a working transport / provider.
49
+ *
50
+ * Typical use: SMTP host, S3 bucket, model id — values without which the
51
+ * downstream SDK would 400 with a cryptic message. The clearer "tenant
52
+ * must configure X via tenant-admin UI" lands at the call-site instead.
53
+ */
54
+ export function requireNonEmpty(
55
+ value: string | undefined,
56
+ featureName: string,
57
+ label: string,
58
+ uiHint = "Set via tenant-admin UI or seed-handler.",
59
+ ): string {
60
+ const defined = requireDefined(value, featureName, label);
61
+ if (defined.length === 0) {
62
+ throw new Error(
63
+ `${featureName}: '${label}' is empty — tenant must configure it before use. ${uiHint}`,
64
+ );
65
+ }
66
+ return defined;
67
+ }
@@ -0,0 +1,4 @@
1
+ // Public API of foundation-shared — utilities consumed by the per-tenant
2
+ // Foundation packages (ai-foundation, mail-foundation, file-foundation).
3
+
4
+ export { requireDefined, requireNonEmpty } from "./config-helpers";
@@ -0,0 +1,194 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import {
3
+ createEncryptionProvider,
4
+ createTenantDb,
5
+ type DbConnection,
6
+ } from "@cosmicdrift/kumiko-framework/db";
7
+ import {
8
+ access,
9
+ createRegistry,
10
+ createTenantConfig,
11
+ defineFeature,
12
+ type Registry,
13
+ type SessionUser,
14
+ } from "@cosmicdrift/kumiko-framework/engine";
15
+ import {
16
+ createArchivedStreamsTable,
17
+ createEventsTable,
18
+ } from "@cosmicdrift/kumiko-framework/event-store";
19
+ import { createJobRunner, type JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
20
+ import {
21
+ createTestDb,
22
+ createTestRedis,
23
+ pushTables,
24
+ type TestDb,
25
+ type TestRedis,
26
+ TestUsers,
27
+ } from "@cosmicdrift/kumiko-framework/stack";
28
+ import { bridgeStub, sleep } from "@cosmicdrift/kumiko-framework/testing";
29
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
30
+ import { ConfigHandlers } from "../../config/constants";
31
+ import { createConfigAccessor, createConfigFeature } from "../../config/feature";
32
+ import { type ConfigResolver, createConfigResolver } from "../../config/resolver";
33
+ import { configValuesTable } from "../../config/table";
34
+
35
+ // --- Setup ---
36
+
37
+ let testDb: TestDb;
38
+ let testRedis: TestRedis;
39
+ let db: DbConnection;
40
+ let registry: Registry;
41
+ let resolver: ConfigResolver;
42
+ let jobRunner: JobRunner;
43
+
44
+ const testEncryptionKey = randomBytes(32).toString("base64");
45
+
46
+ // Feature with a system-only config key and a job that sets it
47
+ const billingFeature = defineFeature("billing", (r) => {
48
+ r.requires("config");
49
+
50
+ r.config({
51
+ keys: {
52
+ monthlyTotal: createTenantConfig("number", {
53
+ default: 0,
54
+ write: access.system,
55
+ read: access.roles("Admin"),
56
+ }),
57
+ },
58
+ });
59
+
60
+ // Job that calculates monthly total and writes it via SYSTEM_USER.
61
+ // Post-ES the write path is the config:write:set handler — the old
62
+ // resolver.set escape hatch is gone. checkWriteAccess grants a
63
+ // SYSTEM_ROLE caller the right to write system-only keys that Admin
64
+ // cannot touch, so the security invariant (see Admin test below) holds.
65
+ r.job("calculateTotal", { trigger: { manual: true } }, async (_payload, ctx) => {
66
+ const systemUser = ctx["systemUser"] as SessionUser;
67
+ const jobDb = ctx["db"] as DbConnection;
68
+ const reg = ctx["registry"] as Registry;
69
+
70
+ ctx.log?.info("Calculating monthly total...");
71
+ const total = 42000;
72
+
73
+ const handler = reg.getWriteHandler("config:write:set");
74
+ if (handler) {
75
+ const parsed = handler.schema.parse({
76
+ key: "billing:config:monthly-total",
77
+ value: total,
78
+ });
79
+ const tenantDb = createTenantDb(jobDb, systemUser.tenantId, "system");
80
+ await handler.handler(
81
+ { type: "config:write:set", payload: parsed, user: systemUser },
82
+ {
83
+ db: tenantDb,
84
+ registry: reg,
85
+ configResolver: ctx["configResolver"] as ConfigResolver,
86
+ ...bridgeStub(),
87
+ },
88
+ );
89
+ }
90
+
91
+ ctx.log?.info(`Set monthlyTotal to ${total}`);
92
+ });
93
+ });
94
+
95
+ const configFeature = createConfigFeature();
96
+
97
+ beforeAll(async () => {
98
+ testDb = await createTestDb();
99
+ testRedis = await createTestRedis();
100
+ db = testDb.db;
101
+
102
+ await pushTables(db, { configValuesTable });
103
+ // Post-ES config writes go through the event-store executor, which needs
104
+ // the framework events + archived-streams tables to exist before the
105
+ // first append. setupTestStack provisions them automatically; this test
106
+ // builds its DB manually (createTestDb + pushTables), so we do it here.
107
+ await createEventsTable(db);
108
+ await createArchivedStreamsTable(db);
109
+
110
+ const encryption = createEncryptionProvider(testEncryptionKey);
111
+ resolver = createConfigResolver({ encryption });
112
+
113
+ registry = createRegistry([configFeature, billingFeature]);
114
+
115
+ const redisUrl = `redis://${testRedis.redis.options.host}:${testRedis.redis.options.port}/${testRedis.redis.options.db}`;
116
+
117
+ jobRunner = createJobRunner({
118
+ registry,
119
+ context: { db, registry, configResolver: resolver, configEncryption: encryption },
120
+ redisUrl,
121
+ consumerLane: "worker",
122
+ queueNamePrefix: `kumiko-system-user-test-${Date.now()}`,
123
+ });
124
+
125
+ await jobRunner.start();
126
+ });
127
+
128
+ afterAll(async () => {
129
+ await jobRunner.stop();
130
+ await testDb.cleanup();
131
+ await testRedis.cleanup();
132
+ });
133
+
134
+ // --- Tests ---
135
+
136
+ describe("SYSTEM_USER in jobs", () => {
137
+ test("job sets system-only config via ctx.systemUser", async () => {
138
+ // Dispatch with tenantId in payload so systemUser gets the right tenant
139
+ await jobRunner.dispatch("billing:job:calculate-total", {
140
+ tenantId: "00000000-0000-4000-8000-000000000001",
141
+ });
142
+ await sleep(1500);
143
+
144
+ // Config should have been set by the job via SYSTEM_USER
145
+ const configFn = createConfigAccessor(
146
+ registry,
147
+ resolver,
148
+ "00000000-0000-4000-8000-000000000001",
149
+ "11111111-0000-4000-8000-000000000099",
150
+ db,
151
+ );
152
+ const total = await configFn("billing:config:monthly-total");
153
+ expect(total).toBe(42000);
154
+ expect(typeof total).toBe("number");
155
+ });
156
+
157
+ test("Admin cannot set system-only config directly", async () => {
158
+ const handler = registry.getWriteHandler(ConfigHandlers.set);
159
+ if (!handler) throw new Error("config.set not found");
160
+
161
+ const parsed = handler.schema.parse({
162
+ key: "billing:config:monthly-total",
163
+ value: 99999,
164
+ });
165
+
166
+ const adminUser = TestUsers.admin;
167
+ const result = await handler.handler(
168
+ { type: ConfigHandlers.set, payload: parsed, user: adminUser },
169
+ {
170
+ db: createTenantDb(db, adminUser.tenantId, "system"),
171
+ registry,
172
+ configResolver: resolver,
173
+ ...bridgeStub(),
174
+ },
175
+ );
176
+
177
+ expect(result.isSuccess).toBe(false);
178
+ if (!result.isSuccess) {
179
+ expect(result.error.code).toBe("access_denied");
180
+ expect(result.error.details).toMatchObject({ reason: "config_key_is_system_only" });
181
+ }
182
+ });
183
+
184
+ test("value set by job is still 42000, not overwritten by Admin attempt", async () => {
185
+ const configFn = createConfigAccessor(
186
+ registry,
187
+ resolver,
188
+ "00000000-0000-4000-8000-000000000001",
189
+ "11111111-0000-4000-8000-000000000001",
190
+ db,
191
+ );
192
+ expect(await configFn("billing:config:monthly-total")).toBe(42000);
193
+ });
194
+ });
@@ -0,0 +1,143 @@
1
+ // Event-shape contract for jobRun aggregate. Pins the three domain
2
+ // events (run-started / run-completed / run-failed) against their
3
+ // registered schemas + the stable type-name constants. A silent rename
4
+ // (event-type-string, aggregateType, or payload-shape) fails here
5
+ // instead of breaking MSP consumers and audit exports.
6
+ //
7
+ // The jobs integration test (jobs-feature.integration.ts) covers the
8
+ // projection side (list + detail queries). This file covers the event
9
+ // side — complementary coverage, minimal overlap.
10
+
11
+ import { createRegistry, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
12
+ import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
13
+ import {
14
+ createTestDb,
15
+ createTestRedis,
16
+ pushTables,
17
+ type TestDb,
18
+ type TestRedis,
19
+ } from "@cosmicdrift/kumiko-framework/stack";
20
+ import { eq } from "drizzle-orm";
21
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
22
+ import { runCompletedSchema, runFailedSchema, runStartedSchema } from "../events";
23
+ import { createJobsFeature } from "../feature";
24
+ import {
25
+ createJobRunLogger,
26
+ JOB_RUN_COMPLETED_EVENT,
27
+ JOB_RUN_FAILED_EVENT,
28
+ JOB_RUN_STARTED_EVENT,
29
+ } from "../job-run-logger";
30
+ import { jobRunLogsTable, jobRunsTable } from "../job-run-table";
31
+
32
+ let testDb: TestDb;
33
+ let testRedis: TestRedis;
34
+ let logger: ReturnType<typeof createJobRunLogger>;
35
+
36
+ beforeAll(async () => {
37
+ testDb = await createTestDb();
38
+ testRedis = await createTestRedis();
39
+ const registry = createRegistry([createJobsFeature()]);
40
+ await pushTables(testDb.db, { jobRunsTable, jobRunLogsTable });
41
+ await createEventsTable(testDb.db);
42
+ logger = createJobRunLogger({ db: testDb.db, registry });
43
+ });
44
+
45
+ afterAll(async () => {
46
+ await testDb.cleanup();
47
+ await testRedis.cleanup();
48
+ });
49
+
50
+ beforeEach(async () => {
51
+ await testDb.db.delete(eventsTable);
52
+ await testDb.db.delete(jobRunsTable);
53
+ await testDb.db.delete(jobRunLogsTable);
54
+ });
55
+
56
+ describe("jobRun event shapes", () => {
57
+ test("event-type constants are stable strings", () => {
58
+ // Guard against silent rename. Tests that subscribe via string-match
59
+ // (MSPs written in userland, audit export tools) break without this.
60
+ expect(JOB_RUN_STARTED_EVENT).toBe("jobs:event:run-started");
61
+ expect(JOB_RUN_COMPLETED_EVENT).toBe("jobs:event:run-completed");
62
+ expect(JOB_RUN_FAILED_EVENT).toBe("jobs:event:run-failed");
63
+ });
64
+
65
+ test("onJobStart writes a run-started event on the jobRun aggregate", async () => {
66
+ await logger.onJobStart?.("example:job:import", "bull-42", {
67
+ triggeredById: "u-99",
68
+ payload: JSON.stringify({ foo: 1 }),
69
+ attempt: 1,
70
+ });
71
+
72
+ const events = await testDb.db
73
+ .select()
74
+ .from(eventsTable)
75
+ .where(eq(eventsTable.type, JOB_RUN_STARTED_EVENT));
76
+
77
+ expect(events.length).toBe(1);
78
+ const e = events[0];
79
+ expect(e?.aggregateType).toBe("jobRun");
80
+ expect(e?.tenantId).toBe(SYSTEM_TENANT_ID);
81
+ // Payload round-trips through the registered schema — drift would
82
+ // fail parse here, not silently land on the stream.
83
+ expect(() => runStartedSchema.parse(e?.payload)).not.toThrow();
84
+ const p = runStartedSchema.parse(e?.payload);
85
+ expect(p.jobName).toBe("example:job:import");
86
+ expect(p.bullJobId).toBe("bull-42");
87
+ expect(p.triggeredById).toBe("u-99");
88
+ expect(p.attempt).toBe(1);
89
+ });
90
+
91
+ test("onJobComplete writes a run-completed event with batched logs", async () => {
92
+ await logger.onJobStart?.("example:job:export", "bull-1", {});
93
+ await logger.onJobComplete?.("example:job:export", "bull-1", 123, [
94
+ { level: "info", message: "started", timestamp: Temporal.Now.instant() },
95
+ { level: "info", message: "done", timestamp: Temporal.Now.instant() },
96
+ ]);
97
+
98
+ const events = await testDb.db
99
+ .select()
100
+ .from(eventsTable)
101
+ .where(eq(eventsTable.type, JOB_RUN_COMPLETED_EVENT));
102
+
103
+ expect(events.length).toBe(1);
104
+ const p = runCompletedSchema.parse(events[0]?.payload);
105
+ expect(p.duration).toBe(123);
106
+ expect(p.logs).toHaveLength(2);
107
+ expect(p.logs[0]?.level).toBe("info");
108
+ });
109
+
110
+ test("onJobFailed writes a run-failed event with error + logs", async () => {
111
+ await logger.onJobStart?.("example:job:fragile", "bull-9", {});
112
+ await logger.onJobFailed?.("example:job:fragile", "bull-9", "boom", [
113
+ { level: "error", message: "kaboom", timestamp: Temporal.Now.instant() },
114
+ ]);
115
+
116
+ const events = await testDb.db
117
+ .select()
118
+ .from(eventsTable)
119
+ .where(eq(eventsTable.type, JOB_RUN_FAILED_EVENT));
120
+
121
+ expect(events.length).toBe(1);
122
+ const p = runFailedSchema.parse(events[0]?.payload);
123
+ expect(p.error).toBe("boom");
124
+ expect(p.logs).toHaveLength(1);
125
+ });
126
+
127
+ test("start + complete both land on the SAME aggregate stream", async () => {
128
+ await logger.onJobStart?.("example:job:stream", "bull-99", {});
129
+ await logger.onJobComplete?.("example:job:stream", "bull-99", 10, []);
130
+
131
+ // Both events should share the same aggregateId — that's what makes
132
+ // the jobRun a single stream and lets ctx.loadAggregate() reduce
133
+ // them into a coherent state.
134
+ const events = await testDb.db
135
+ .select()
136
+ .from(eventsTable)
137
+ .where(eq(eventsTable.aggregateType, "jobRun"));
138
+
139
+ expect(events.length).toBe(2);
140
+ const ids = new Set(events.map((e) => e.aggregateId));
141
+ expect(ids.size).toBe(1);
142
+ });
143
+ });