@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,235 @@
1
+ // Full-stack integration test for file-foundation. Drives the
2
+ // provider-factory through the dispatcher so the real config-resolver
3
+ // + secrets-context + tenant-scoped reads are exercised.
4
+
5
+ import { randomBytes } from "node:crypto";
6
+ import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
7
+ import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
8
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
9
+ import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
10
+ import {
11
+ createEntityTable,
12
+ createTestUser,
13
+ pushTables,
14
+ setupTestStack,
15
+ type TestStack,
16
+ testTenantId,
17
+ } from "@cosmicdrift/kumiko-framework/stack";
18
+ import {
19
+ createMutableMasterKeyProvider,
20
+ type MutableMasterKeyProvider,
21
+ } from "@cosmicdrift/kumiko-framework/testing";
22
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
23
+ import { z } from "zod";
24
+ import { createConfigFeature } from "../../config";
25
+ import { ConfigHandlers } from "../../config/constants";
26
+ import { createConfigAccessorFactory } from "../../config/feature";
27
+ import { type ConfigResolver, createConfigResolver } from "../../config/resolver";
28
+ import { configValuesTable } from "../../config/table";
29
+ import { fileProviderS3Feature, S3_SECRET_ACCESS_KEY } from "../../file-provider-s3";
30
+ import { createSecretsContext, createSecretsFeature, tenantSecretsTable } from "../../secrets";
31
+ import { createTenantFeature } from "../../tenant/feature";
32
+ import { tenantEntity } from "../../tenant/schema/tenant";
33
+ import { createFileProviderForTenant, fileFoundationFeature } from "../feature";
34
+
35
+ // --- Test-Handler that exercises the factory end-to-end ---
36
+
37
+ const TEST_HANDLER_QN = "file-test:write:build-provider";
38
+ const testProbeFeature = defineFeature("file-test", (r) => {
39
+ r.requires("config");
40
+ r.requires("secrets");
41
+ r.writeHandler(
42
+ defineWriteHandler({
43
+ name: "build-provider",
44
+ schema: z.object({}),
45
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
46
+ handler: async (event, ctx) => {
47
+ const provider = await createFileProviderForTenant(
48
+ ctx,
49
+ event.user.tenantId,
50
+ TEST_HANDLER_QN,
51
+ );
52
+ return {
53
+ isSuccess: true,
54
+ data: {
55
+ hasWrite: typeof provider.write === "function",
56
+ hasRead: typeof provider.read === "function",
57
+ hasDelete: typeof provider.delete === "function",
58
+ },
59
+ };
60
+ },
61
+ }),
62
+ );
63
+ });
64
+
65
+ // --- Setup ---
66
+
67
+ let stack: TestStack;
68
+ let db: DbConnection;
69
+ let resolver: ConfigResolver;
70
+ let providerRef: MutableMasterKeyProvider;
71
+
72
+ const testEncryptionKey = randomBytes(32).toString("base64");
73
+
74
+ beforeAll(async () => {
75
+ const encryption = createEncryptionProvider(testEncryptionKey);
76
+ resolver = createConfigResolver({ encryption });
77
+
78
+ const initialKp = createEnvMasterKeyProvider({
79
+ env: {
80
+ KUMIKO_SECRETS_MASTER_KEY_V1: randomBytes(32).toString("base64"),
81
+ KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "1",
82
+ },
83
+ });
84
+ providerRef = createMutableMasterKeyProvider(initialKp);
85
+
86
+ stack = await setupTestStack({
87
+ features: [
88
+ createConfigFeature(),
89
+ createTenantFeature(),
90
+ createSecretsFeature(),
91
+ fileFoundationFeature,
92
+ fileProviderS3Feature,
93
+ testProbeFeature,
94
+ ],
95
+ masterKeyProvider: providerRef,
96
+ extraContext: ({ db, registry }) => ({
97
+ configResolver: resolver,
98
+ configEncryption: encryption,
99
+ _configAccessorFactory: createConfigAccessorFactory(registry, resolver),
100
+ secrets: createSecretsContext({ db, masterKeyProvider: providerRef }),
101
+ }),
102
+ });
103
+ db = stack.db;
104
+
105
+ await createEntityTable(db, tenantEntity);
106
+ await pushTables(db, { configValuesTable, tenant_secrets: tenantSecretsTable });
107
+ await createEventsTable(db);
108
+ });
109
+
110
+ afterAll(async () => {
111
+ await stack.cleanup();
112
+ });
113
+
114
+ function adminFor(tenantNumber: number) {
115
+ return createTestUser({
116
+ id: tenantNumber,
117
+ tenantId: testTenantId(tenantNumber),
118
+ roles: ["TenantAdmin", "SystemAdmin"],
119
+ });
120
+ }
121
+
122
+ async function setConfig(admin: ReturnType<typeof adminFor>, key: string, value: unknown) {
123
+ await stack.http.writeOk(ConfigHandlers.set, { key, value }, admin);
124
+ }
125
+
126
+ /** Set the file-foundation provider-selector to "s3". */
127
+ async function selectS3Provider(admin: ReturnType<typeof adminFor>) {
128
+ await setConfig(admin, "file-foundation:config:provider", "s3");
129
+ }
130
+
131
+ // --- Scenario 1: full happy-path roundtrip (Hetzner Object Storage shape) ---
132
+
133
+ describe("scenario 1: happy path", () => {
134
+ test("admin sets config + secret → factory builds working file-storage provider", async () => {
135
+ const admin = adminFor(501);
136
+
137
+ await selectS3Provider(admin);
138
+ // Hetzner Object Storage typical config — covers MinIO/R2/S3 too via
139
+ // endpoint + forcePathStyle. AccessKeyId is public-ish, secret goes
140
+ // into the encrypted secrets store.
141
+ await setConfig(admin, "file-provider-s3:config:bucket", "test-bucket");
142
+ await setConfig(admin, "file-provider-s3:config:region", "fsn1");
143
+ await setConfig(
144
+ admin,
145
+ "file-provider-s3:config:endpoint",
146
+ "https://fsn1.your-objectstorage.com",
147
+ );
148
+ await setConfig(admin, "file-provider-s3:config:force-path-style", true);
149
+ await setConfig(admin, "file-provider-s3:config:access-key-id", "AKIATEST123");
150
+
151
+ await stack.http.writeOk(
152
+ "secrets:write:set",
153
+ { key: S3_SECRET_ACCESS_KEY.name, value: "secret-key-not-actually-real" },
154
+ admin,
155
+ );
156
+
157
+ const result = (await stack.http.writeOk(TEST_HANDLER_QN, {}, admin)) as Record<
158
+ string,
159
+ unknown
160
+ >;
161
+ expect(result["hasWrite"]).toBe(true);
162
+ expect(result["hasRead"]).toBe(true);
163
+ expect(result["hasDelete"]).toBe(true);
164
+ });
165
+ });
166
+
167
+ // --- Scenario 2: validation errors ---
168
+
169
+ describe("scenario 2: validation errors", () => {
170
+ test("missing bucket → factory throws with hint instead of cryptic SDK error", async () => {
171
+ const admin = adminFor(502);
172
+
173
+ await selectS3Provider(admin);
174
+ // Set everything except bucket. requireNonEmpty rejects with a clear
175
+ // message naming `bucket`.
176
+ await setConfig(admin, "file-provider-s3:config:region", "us-east-1");
177
+ await setConfig(admin, "file-provider-s3:config:access-key-id", "AKIATEST");
178
+ await stack.http.writeOk(
179
+ "secrets:write:set",
180
+ { key: S3_SECRET_ACCESS_KEY.name, value: "secret" },
181
+ admin,
182
+ );
183
+
184
+ const error = await stack.http.writeErr(TEST_HANDLER_QN, {}, admin);
185
+ expect(JSON.stringify(error)).toMatch(/'bucket' is empty/);
186
+ });
187
+
188
+ test("missing secret-access-key → factory throws naming the secret", async () => {
189
+ const admin = adminFor(503);
190
+
191
+ await selectS3Provider(admin);
192
+ await setConfig(admin, "file-provider-s3:config:bucket", "b");
193
+ await setConfig(admin, "file-provider-s3:config:region", "us-east-1");
194
+ await setConfig(admin, "file-provider-s3:config:access-key-id", "AKIATEST");
195
+ // Skip the secret. Factory throws referencing S3_SECRET_ACCESS_KEY.name.
196
+
197
+ const error = await stack.http.writeErr(TEST_HANDLER_QN, {}, admin);
198
+ expect(JSON.stringify(error)).toMatch(/s3-secret-access-key/);
199
+ });
200
+ });
201
+
202
+ // --- Scenario 3: tenant isolation ---
203
+
204
+ describe("scenario 3: tenant isolation", () => {
205
+ test("tenant A's S3 config doesn't bleed into tenant B's provider", async () => {
206
+ const adminA = adminFor(504);
207
+ const adminB = adminFor(505);
208
+
209
+ await selectS3Provider(adminA);
210
+ await selectS3Provider(adminB);
211
+
212
+ await setConfig(adminA, "file-provider-s3:config:bucket", "tenant-a-bucket");
213
+ await setConfig(adminA, "file-provider-s3:config:region", "fsn1");
214
+ await setConfig(adminA, "file-provider-s3:config:access-key-id", "A-KEY");
215
+ await stack.http.writeOk(
216
+ "secrets:write:set",
217
+ { key: S3_SECRET_ACCESS_KEY.name, value: "secret-a" },
218
+ adminA,
219
+ );
220
+
221
+ await setConfig(adminB, "file-provider-s3:config:bucket", "tenant-b-bucket");
222
+ await setConfig(adminB, "file-provider-s3:config:region", "us-east-1");
223
+ await setConfig(adminB, "file-provider-s3:config:access-key-id", "B-KEY");
224
+ await stack.http.writeOk(
225
+ "secrets:write:set",
226
+ { key: S3_SECRET_ACCESS_KEY.name, value: "secret-b" },
227
+ adminB,
228
+ );
229
+
230
+ const a = (await stack.http.writeOk(TEST_HANDLER_QN, {}, adminA)) as Record<string, unknown>;
231
+ const b = (await stack.http.writeOk(TEST_HANDLER_QN, {}, adminB)) as Record<string, unknown>;
232
+ expect(a["hasWrite"]).toBe(true);
233
+ expect(b["hasWrite"]).toBe(true);
234
+ });
235
+ });
@@ -0,0 +1,123 @@
1
+ // kumiko-feature-version: 1
2
+ //
3
+ // file-foundation as a Kumiko bundled feature — plugin-API shape.
4
+ //
5
+ // **Pattern-Vorbild:** identisch zu `mail-foundation`. Foundation
6
+ // deklariert extension-point `fileProvider`, Provider-Features (file-
7
+ // provider-s3, später file-provider-azure-blob, file-provider-gcs)
8
+ // registrieren sich namentlich. Tenant wählt zur Runtime via config-
9
+ // key `provider`.
10
+ //
11
+ // **Was diese Foundation NICHT mehr macht:**
12
+ // - Keine S3-spezifischen Config-Keys mehr (bucket/region/endpoint/
13
+ // forcePathStyle/accessKeyId) — die leben im Provider-Plugin.
14
+ // - Kein direkter Import von `createS3Provider`. Foundation kennt
15
+ // nur das `FileStorageProvider`-Interface (Type-Import, kein
16
+ // runtime-coupling).
17
+ //
18
+ // **Standalone:** Foundation ist ohne tier-engine nutzbar. Existing
19
+ // `files-provider-s3` (App-wide-Library) bleibt unangetastet.
20
+ //
21
+ // **Boot-Dependencies:** config (für provider-selector). Kein secrets,
22
+ // weil Foundation selbst keine Secrets hält.
23
+
24
+ import { requireDefined } from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
25
+ import {
26
+ access,
27
+ createTenantConfig,
28
+ defineFeature,
29
+ type HandlerContext,
30
+ } from "@cosmicdrift/kumiko-framework/engine";
31
+ import type { FileStorageProvider } from "@cosmicdrift/kumiko-framework/files";
32
+
33
+ const FEATURE_NAME = "file-foundation";
34
+
35
+ // =============================================================================
36
+ // Plugin-Interface — what a Provider-Plugin must implement
37
+ // =============================================================================
38
+
39
+ /**
40
+ * File-Storage-Plugin contract. Each provider-feature (file-provider-s3,
41
+ * file-provider-azure-blob, ...) registers an implementation via
42
+ * `r.useExtension("fileProvider", "<name>", { build })`.
43
+ */
44
+ export type FileProviderPlugin = {
45
+ readonly build: (ctx: HandlerContext, tenantId: string) => Promise<FileStorageProvider>;
46
+ };
47
+
48
+ // =============================================================================
49
+ // Feature-definition
50
+ // =============================================================================
51
+
52
+ export const fileFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
53
+ r.requires("config");
54
+
55
+ r.extendsRegistrar("fileProvider", {
56
+ onRegister: () => {
57
+ // No side-effects at register-time — registry stores the usage,
58
+ // factory looks it up at request-time.
59
+ },
60
+ });
61
+
62
+ const configKeys = r.config({
63
+ keys: {
64
+ provider: createTenantConfig("text", {
65
+ default: "",
66
+ write: access.roles("TenantAdmin", "SystemAdmin"),
67
+ read: access.roles("TenantAdmin", "SystemAdmin", "User"),
68
+ }),
69
+ },
70
+ });
71
+
72
+ return { configKeys };
73
+ });
74
+
75
+ // =============================================================================
76
+ // Provider-factory — looks up the registered plugin + delegates
77
+ // =============================================================================
78
+
79
+ export async function createFileProviderForTenant(
80
+ ctx: HandlerContext,
81
+ tenantId: string,
82
+ handlerName = "file-foundation:provider-factory",
83
+ ): Promise<FileStorageProvider> {
84
+ const ctxConfig = ctx.config;
85
+ if (!ctxConfig) {
86
+ throw new Error(
87
+ `${handlerName}: ctx.config is missing — feature requires the config-feature mounted in the registry`,
88
+ );
89
+ }
90
+ if (!ctx.registry) {
91
+ throw new Error(
92
+ `${handlerName}: ctx.registry is missing — required to look up registered file-provider plugins`,
93
+ );
94
+ }
95
+
96
+ const provider = requireDefined(
97
+ await ctxConfig(fileFoundationFeature.exports.configKeys.provider),
98
+ FEATURE_NAME,
99
+ "provider",
100
+ ) as string;
101
+ if (provider.length === 0) {
102
+ const usages = ctx.registry.getExtensionUsages("fileProvider");
103
+ const known = usages.map((u) => u.entityName).join(", ") || "<none>";
104
+ throw new Error(
105
+ `${FEATURE_NAME}: no provider selected — set the 'provider' config-key to one of: ${known}. ` +
106
+ `Mount a file-provider-* feature first if no plugins are registered.`,
107
+ );
108
+ }
109
+
110
+ const usages = ctx.registry.getExtensionUsages("fileProvider");
111
+ const usage = usages.find((u) => u.entityName === provider);
112
+ if (!usage) {
113
+ const known = usages.map((u) => u.entityName).join(", ") || "<none>";
114
+ throw new Error(
115
+ `${FEATURE_NAME}: provider "${provider}" not registered. Known: ${known}. ` +
116
+ `Mount the matching file-provider-${provider} feature.`,
117
+ );
118
+ }
119
+
120
+ // @cast-boundary engine-payload — extension-usage carries unknown options
121
+ const plugin = usage.options as FileProviderPlugin;
122
+ return plugin.build(ctx, tenantId);
123
+ }
@@ -0,0 +1,7 @@
1
+ // Public API of the file-foundation bundled-feature.
2
+
3
+ export {
4
+ createFileProviderForTenant,
5
+ type FileProviderPlugin,
6
+ fileFoundationFeature,
7
+ } from "./feature";
@@ -0,0 +1,35 @@
1
+ // feature.ts contract tests for file-provider-inmemory.
2
+
3
+ import { describe, expect, test } from "vitest";
4
+ import { clearStorage, fileProviderInMemoryFeature, listKeys } from "../feature";
5
+
6
+ describe("fileProviderInMemoryFeature — shape", () => {
7
+ test("has the expected name", () => {
8
+ expect(fileProviderInMemoryFeature.name).toBe("file-provider-inmemory");
9
+ });
10
+
11
+ test("requires only file-foundation (no config, no secrets)", () => {
12
+ expect(fileProviderInMemoryFeature.requires).toContain("file-foundation");
13
+ expect(fileProviderInMemoryFeature.requires).not.toContain("config");
14
+ expect(fileProviderInMemoryFeature.requires).not.toContain("secrets");
15
+ });
16
+ });
17
+
18
+ describe("fileProviderInMemoryFeature — plugin-registration", () => {
19
+ test("registers itself under entityName 'inmemory' for file-foundation's extension", () => {
20
+ const usages = fileProviderInMemoryFeature.extensionUsages;
21
+ expect(
22
+ usages.some((u) => u.extensionName === "fileProvider" && u.entityName === "inmemory"),
23
+ ).toBe(true);
24
+ });
25
+ });
26
+
27
+ describe("listKeys / clearStorage — per-tenant store helpers", () => {
28
+ test("listKeys liefert empty-array für unbekannten Tenant", () => {
29
+ expect(listKeys("never-touched")).toEqual([]);
30
+ });
31
+
32
+ test("clearStorage auf unbekannten Tenant ist no-op", () => {
33
+ expect(() => clearStorage("never-touched")).not.toThrow();
34
+ });
35
+ });
@@ -0,0 +1,73 @@
1
+ // kumiko-feature-version: 1
2
+ //
3
+ // file-provider-inmemory — In-Memory-FileProvider für die file-
4
+ // foundation Plugin-API. Speichert Files in einem per-Tenant-Map
5
+ // statt in S3/Hetzner-Object-Storage. Für Demos, Sample-Apps und
6
+ // Tests ohne MinIO-Container.
7
+ //
8
+ // **Was diese Feature liefert:**
9
+ // 1. Plugin-Registration via `r.useExtension("fileProvider",
10
+ // "inmemory", { build })`.
11
+ // 2. **Pro-Tenant Storage.** Jeder Tenant kriegt einen eigenen
12
+ // InMemoryFileProvider — Tenant-Isolation by-design, keine
13
+ // Pfad-Konvention nötig.
14
+ //
15
+ // **Pattern-Vorbild:** mirrors file-provider-s3.
16
+ //
17
+ // **NICHT für Production.** Buffer ist Process-Memory, geht beim
18
+ // Restart verloren + wächst monoton mit jedem write.
19
+
20
+ import type { FileProviderPlugin } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
21
+ import { defineFeature, type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
22
+ import {
23
+ createInMemoryFileProvider,
24
+ type FileStorageProvider,
25
+ type InMemoryFileProvider,
26
+ } from "@cosmicdrift/kumiko-framework/files";
27
+
28
+ const FEATURE_NAME = "file-provider-inmemory";
29
+
30
+ // =============================================================================
31
+ // Per-tenant in-memory store
32
+ // =============================================================================
33
+
34
+ const providersByTenant = new Map<string, InMemoryFileProvider>();
35
+
36
+ function getOrCreateProviderForTenant(tenantId: string): InMemoryFileProvider {
37
+ let provider = providersByTenant.get(tenantId);
38
+ if (!provider) {
39
+ provider = createInMemoryFileProvider();
40
+ providersByTenant.set(tenantId, provider);
41
+ }
42
+ return provider;
43
+ }
44
+
45
+ /** Demo/Test-Helper: liste die Keys eines Tenant-Storage. */
46
+ export function listKeys(tenantId: string): readonly string[] {
47
+ return providersByTenant.get(tenantId)?.keys() ?? [];
48
+ }
49
+
50
+ /** Demo/Test-Helper: leere den Tenant-Storage. */
51
+ export function clearStorage(tenantId: string): void {
52
+ providersByTenant.get(tenantId)?.clear();
53
+ }
54
+
55
+ // =============================================================================
56
+ // Feature-definition
57
+ // =============================================================================
58
+
59
+ export const fileProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
60
+ // Kein r.requires("config") + kein r.requires("secrets") — der
61
+ // In-Memory-Provider hat keine Config + kein Secret. Nur die
62
+ // file-foundation muss da sein (Plugin-extension-point).
63
+ r.requires("file-foundation");
64
+
65
+ const plugin: FileProviderPlugin = {
66
+ build: async (_ctx: HandlerContext, tenantId: string): Promise<FileStorageProvider> => {
67
+ // Returnt den per-tenant Storage. Identitätsstabil zwischen calls
68
+ // damit accumulated state erhalten bleibt.
69
+ return getOrCreateProviderForTenant(tenantId);
70
+ },
71
+ };
72
+ r.useExtension("fileProvider", "inmemory", plugin);
73
+ });
@@ -0,0 +1,3 @@
1
+ // Public API of the file-provider-inmemory bundled-feature.
2
+
3
+ export { clearStorage, fileProviderInMemoryFeature, listKeys } from "./feature";
@@ -0,0 +1,54 @@
1
+ // feature.ts contract tests for file-provider-s3.
2
+
3
+ import { describe, expect, test } from "vitest";
4
+ import { fileProviderS3Feature, S3_SECRET_ACCESS_KEY } from "../feature";
5
+
6
+ describe("fileProviderS3Feature — shape", () => {
7
+ test("has the expected name", () => {
8
+ expect(fileProviderS3Feature.name).toBe("file-provider-s3");
9
+ });
10
+
11
+ test("requires config + secrets + file-foundation", () => {
12
+ expect(fileProviderS3Feature.requires).toContain("config");
13
+ expect(fileProviderS3Feature.requires).toContain("secrets");
14
+ expect(fileProviderS3Feature.requires).toContain("file-foundation");
15
+ });
16
+ });
17
+
18
+ describe("fileProviderS3Feature.exports — typed handles", () => {
19
+ test("exports.configKeys covers the S3-config knobs", () => {
20
+ const keys = fileProviderS3Feature.exports.configKeys;
21
+ expect(keys.bucket).toBeDefined();
22
+ expect(keys.region).toBeDefined();
23
+ expect(keys.endpoint).toBeDefined();
24
+ expect(keys.forcePathStyle).toBeDefined();
25
+ expect(keys.accessKeyId).toBeDefined();
26
+ });
27
+
28
+ test("exports.secretAccessKey is the S3_SECRET_ACCESS_KEY secret-handle (drift-pin)", () => {
29
+ expect(fileProviderS3Feature.exports.secretAccessKey).toBe(S3_SECRET_ACCESS_KEY);
30
+ expect(S3_SECRET_ACCESS_KEY.name).toBe("file-provider-s3:secret:s3-secret-access-key");
31
+ });
32
+ });
33
+
34
+ describe("S3_SECRET_ACCESS_KEY — generic redaction", () => {
35
+ const secretDef = fileProviderS3Feature.secretKeys["s3.secretAccessKey"];
36
+
37
+ test("redact preserves first 4 + last 4 chars on long keys", () => {
38
+ expect(secretDef?.redact).toBeDefined();
39
+ expect(secretDef?.redact?.("AKIA1234567890ABCDEFGHIJ7890klmn")).toMatch(/^AKIA\.\.\.klmn$/);
40
+ });
41
+
42
+ test("redact masks short keys completely", () => {
43
+ expect(secretDef?.redact?.("short")).toBe("•".repeat(5));
44
+ });
45
+ });
46
+
47
+ describe("fileProviderS3Feature — plugin-registration", () => {
48
+ test("registers itself under entityName 's3' for file-foundation's extension", () => {
49
+ const usages = fileProviderS3Feature.extensionUsages;
50
+ expect(usages.some((u) => u.extensionName === "fileProvider" && u.entityName === "s3")).toBe(
51
+ true,
52
+ );
53
+ });
54
+ });