@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
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@cosmicdrift/kumiko-bundled-features",
3
+ "version": "0.1.0",
4
+ "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
+ "license": "BUSL-1.1",
6
+ "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cosmicdriftgamestudio/kumiko-framework.git",
10
+ "directory": "packages/bundled-features"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/cosmicdriftgamestudio/kumiko-framework/issues"
14
+ },
15
+ "homepage": "https://kumiko.so",
16
+ "type": "module",
17
+ "kumiko": {
18
+ "runtime": "runtime"
19
+ },
20
+ "exports": {
21
+ "./audit": "./src/audit/index.ts",
22
+ "./config": "./src/config/index.ts",
23
+ "./jobs": "./src/jobs/index.ts",
24
+ "./tier-engine": "./src/tier-engine/index.ts",
25
+ "./cap-counter": "./src/cap-counter/index.ts",
26
+ "./billing-foundation": "./src/billing-foundation/index.ts",
27
+ "./subscription-stripe": "./src/subscription-stripe/index.ts",
28
+ "./subscription-mollie": "./src/subscription-mollie/index.ts",
29
+ "./foundation-shared": "./src/foundation-shared/index.ts",
30
+ "./mail-foundation": "./src/mail-foundation/index.ts",
31
+ "./mail-transport-smtp": "./src/mail-transport-smtp/index.ts",
32
+ "./mail-transport-inmemory": "./src/mail-transport-inmemory/index.ts",
33
+ "./file-foundation": "./src/file-foundation/index.ts",
34
+ "./file-provider-s3": "./src/file-provider-s3/index.ts",
35
+ "./file-provider-inmemory": "./src/file-provider-inmemory/index.ts",
36
+ "./tenant": "./src/tenant/index.ts",
37
+ "./tenant/constants": "./src/tenant/constants.ts",
38
+ "./tenant/seeding": "./src/tenant/seeding.ts",
39
+ "./tenant/testing": "./src/tenant/testing.ts",
40
+ "./user": "./src/user/index.ts",
41
+ "./user/seeding": "./src/user/seeding.ts",
42
+ "./user/testing": "./src/user/testing.ts",
43
+ "./auth-email-password": "./src/auth-email-password/index.ts",
44
+ "./auth-email-password/constants": "./src/auth-email-password/constants.ts",
45
+ "./auth-email-password/seeding": "./src/auth-email-password/seeding.ts",
46
+ "./auth-email-password/testing": "./src/auth-email-password/testing.ts",
47
+ "./auth-email-password/web": "./src/auth-email-password/web/index.ts",
48
+ "./delivery": "./src/delivery/index.ts",
49
+ "./channel-in-app": "./src/channel-in-app/index.ts",
50
+ "./channel-email": "./src/channel-email/index.ts",
51
+ "./channel-push": "./src/channel-push/index.ts",
52
+ "./renderer-simple": "./src/renderer-simple/index.ts",
53
+ "./files-provider-s3": "./src/files-provider-s3/index.ts",
54
+ "./rate-limiting": "./src/rate-limiting/index.ts",
55
+ "./secrets": "./src/secrets/index.ts",
56
+ "./sessions": "./src/sessions/index.ts",
57
+ "./sessions/testing": "./src/sessions/testing.ts",
58
+ "./feature-toggles": "./src/feature-toggles/index.ts",
59
+ "./text-content": "./src/text-content/index.ts",
60
+ "./text-content/seeding": "./src/text-content/seeding.ts",
61
+ "./legal-pages": "./src/legal-pages/index.ts"
62
+ },
63
+ "dependencies": {
64
+ "@aws-sdk/client-s3": "^3.700.0",
65
+ "@aws-sdk/s3-request-presigner": "^3.700.0",
66
+ "@cosmicdrift/kumiko-dispatcher-live": "workspace:*",
67
+ "@cosmicdrift/kumiko-framework": "workspace:*",
68
+ "@cosmicdrift/kumiko-renderer": "workspace:*",
69
+ "@cosmicdrift/kumiko-renderer-web": "workspace:*",
70
+ "@mollie/api-client": "^4.5.0",
71
+ "@node-rs/argon2": "^2.0.2",
72
+ "@types/nodemailer": "^8.0.0",
73
+ "clsx": "^2.1.1",
74
+ "lucide-react": "^1.11.0",
75
+ "marked": "^14.1.3",
76
+ "nodemailer": "^8.0.7",
77
+ "react": "^19.2.0",
78
+ "stripe": "^22.1.0",
79
+ "tailwind-merge": "^3.0.2"
80
+ },
81
+ "publishConfig": {
82
+ "registry": "https://registry.npmjs.org",
83
+ "access": "public"
84
+ },
85
+ "files": [
86
+ "src",
87
+ "README.md",
88
+ "LICENSE"
89
+ ]
90
+ }
@@ -0,0 +1,328 @@
1
+ // Audit query — filter-by-example coverage over the event-store. The event
2
+ // log IS the audit trail; this suite proves the query handler exposes the
3
+ // right slices of it (tenant-isolated, filtered, paginated, content-intact).
4
+
5
+ import {
6
+ createEntity,
7
+ createTextField,
8
+ defineEntityWriteHandler,
9
+ defineFeature,
10
+ type SessionUser,
11
+ } from "@cosmicdrift/kumiko-framework/engine";
12
+ import {
13
+ createEntityTable,
14
+ createTestUser,
15
+ resetEventStore,
16
+ setupTestStack,
17
+ type TestStack,
18
+ TestUsers,
19
+ testTenantId,
20
+ } from "@cosmicdrift/kumiko-framework/stack";
21
+ import { sql } from "drizzle-orm";
22
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
23
+ import { AuditQueries } from "../constants";
24
+ import { createAuditFeature } from "../feature";
25
+
26
+ const widgetEntity = createEntity({
27
+ table: "audit_widgets",
28
+ fields: {
29
+ name: createTextField({ required: true }),
30
+ color: createTextField(),
31
+ },
32
+ });
33
+
34
+ const widgetFeature = defineFeature("widgets", (r) => {
35
+ r.entity("widget", widgetEntity);
36
+ for (const verb of ["create", "update", "delete"] as const) {
37
+ r.writeHandler(
38
+ defineEntityWriteHandler(`widget:${verb}`, widgetEntity, {
39
+ access: { roles: ["Admin", "User", "SystemAdmin"] },
40
+ }),
41
+ );
42
+ }
43
+ });
44
+
45
+ let stack: TestStack;
46
+
47
+ const admin = TestUsers.systemAdmin;
48
+ const regularUser: SessionUser = createTestUser({
49
+ id: 7,
50
+ tenantId: testTenantId(1),
51
+ roles: ["User"],
52
+ });
53
+ const otherTenantAdmin: SessionUser = createTestUser({
54
+ id: 8,
55
+ tenantId: testTenantId(2),
56
+ roles: ["Admin"],
57
+ });
58
+
59
+ beforeAll(async () => {
60
+ stack = await setupTestStack({
61
+ features: [widgetFeature, createAuditFeature()],
62
+ });
63
+ await createEntityTable(stack.db, widgetEntity);
64
+ });
65
+
66
+ afterAll(async () => {
67
+ await stack.cleanup();
68
+ });
69
+
70
+ beforeEach(async () => {
71
+ // Fresh event log per test — the audit query reads the events table
72
+ // directly, so stale events from previous tests would leak into results.
73
+ await resetEventStore(stack);
74
+ await stack.db.execute(sql`TRUNCATE audit_widgets`);
75
+ });
76
+
77
+ async function createWidget(user: SessionUser, name: string, color?: string): Promise<string> {
78
+ const res = await stack.http.writeOk<{ id: string }>(
79
+ "widgets:write:widget:create",
80
+ { name, ...(color && { color }) },
81
+ user,
82
+ );
83
+ return res.id;
84
+ }
85
+
86
+ type AuditRow = {
87
+ id: string;
88
+ aggregateId: string;
89
+ aggregateType: string;
90
+ type: string;
91
+ createdBy: string;
92
+ createdAt: string;
93
+ payload: Record<string, unknown>;
94
+ metadata: Record<string, unknown>;
95
+ };
96
+
97
+ type AuditResponse = { rows: AuditRow[]; nextBefore: string | null };
98
+
99
+ describe("audit: list query", () => {
100
+ test("returns events of the caller's tenant, newest first", async () => {
101
+ await createWidget(admin, "A");
102
+ await createWidget(admin, "B");
103
+ await createWidget(admin, "C");
104
+
105
+ const res = await stack.http.queryOk<AuditResponse>(AuditQueries.list, {}, admin);
106
+ expect(res.rows.length).toBeGreaterThanOrEqual(3);
107
+ const names = res.rows.map((r) => r.type);
108
+ expect(names).toContain("widget.created");
109
+ // Descending by id (bigserial) ⇒ newest first.
110
+ for (let i = 1; i < res.rows.length; i++) {
111
+ const prev = BigInt(res.rows[i - 1]!.id);
112
+ const curr = BigInt(res.rows[i]!.id);
113
+ expect(prev > curr).toBe(true);
114
+ }
115
+ });
116
+
117
+ test("tenant isolation: admin on tenant-1 sees NO events from tenant-2", async () => {
118
+ await createWidget(admin, "on-tenant-1");
119
+ await stack.http.writeOk<{ id: string }>(
120
+ "widgets:write:widget:create",
121
+ { name: "on-tenant-2" },
122
+ otherTenantAdmin,
123
+ );
124
+
125
+ const res = await stack.http.queryOk<AuditResponse>(AuditQueries.list, {}, admin);
126
+ for (const r of res.rows) {
127
+ // Only admin's rows come back — the cross-tenant event's createdBy
128
+ // would be the other-tenant admin's id.
129
+ expect(r.createdBy).toBe(admin.id);
130
+ }
131
+ });
132
+
133
+ test("filter by eventType", async () => {
134
+ const id1 = await createWidget(admin, "X");
135
+ await stack.http.writeOk(
136
+ "widgets:write:widget:update",
137
+ { id: id1, version: 1, changes: { color: "red" } },
138
+ admin,
139
+ );
140
+ await stack.http.writeOk("widgets:write:widget:delete", { id: id1 }, admin);
141
+
142
+ const updates = await stack.http.queryOk<AuditResponse>(
143
+ AuditQueries.list,
144
+ { eventType: "widget.updated" },
145
+ admin,
146
+ );
147
+ expect(updates.rows).toHaveLength(1);
148
+ expect(updates.rows[0]?.type).toBe("widget.updated");
149
+
150
+ const deletes = await stack.http.queryOk<AuditResponse>(
151
+ AuditQueries.list,
152
+ { eventType: "widget.deleted" },
153
+ admin,
154
+ );
155
+ expect(deletes.rows).toHaveLength(1);
156
+ expect(deletes.rows[0]?.type).toBe("widget.deleted");
157
+ });
158
+
159
+ test("filter by aggregateId pins the event chain for one entity", async () => {
160
+ const a = await createWidget(admin, "A");
161
+ const b = await createWidget(admin, "B");
162
+ await stack.http.writeOk(
163
+ "widgets:write:widget:update",
164
+ { id: a, version: 1, changes: { color: "blue" } },
165
+ admin,
166
+ );
167
+
168
+ const res = await stack.http.queryOk<AuditResponse>(
169
+ AuditQueries.list,
170
+ { aggregateId: a },
171
+ admin,
172
+ );
173
+ expect(res.rows).toHaveLength(2);
174
+ expect(res.rows.every((r) => r.aggregateId === a)).toBe(true);
175
+ expect(res.rows.some((r) => r.aggregateId === b)).toBe(false);
176
+ });
177
+
178
+ test("filter by userId", async () => {
179
+ await createWidget(admin, "by admin");
180
+ await createWidget(regularUser, "by user");
181
+
182
+ const res = await stack.http.queryOk<AuditResponse>(
183
+ AuditQueries.list,
184
+ { userId: regularUser.id },
185
+ admin,
186
+ );
187
+ expect(res.rows).toHaveLength(1);
188
+ expect(res.rows[0]?.createdBy).toBe(regularUser.id);
189
+ });
190
+
191
+ test("filter by from/to date range (inclusive bounds, outside-range rows excluded)", async () => {
192
+ // Events are written with server-now at ms precision. Delays between
193
+ // writes + anchor timestamps give us clean sort-order. The anchors are
194
+ // captured OUTSIDE the write bursts so precision-truncation on the
195
+ // db side (ms) can't blur anchor vs event.
196
+ await createWidget(admin, "before-window");
197
+ await new Promise((r) => setTimeout(r, 50));
198
+ const t1 = Temporal.Now.instant();
199
+ await new Promise((r) => setTimeout(r, 10));
200
+ await createWidget(admin, "in-window-1");
201
+ await createWidget(admin, "in-window-2");
202
+ await new Promise((r) => setTimeout(r, 50));
203
+ const t2 = Temporal.Now.instant();
204
+ await new Promise((r) => setTimeout(r, 10));
205
+ await createWidget(admin, "after-window");
206
+
207
+ // Slice strictly to the [t1, t2] window — should return exactly 2 rows.
208
+ const inWindow = await stack.http.queryOk<AuditResponse>(
209
+ AuditQueries.list,
210
+ { from: t1.toString(), to: t2.toString() },
211
+ admin,
212
+ );
213
+ expect(inWindow.rows).toHaveLength(2);
214
+ const names = inWindow.rows.map((r) => (r.payload as { name?: string }).name).sort();
215
+ expect(names).toEqual(["in-window-1", "in-window-2"]);
216
+
217
+ // From-only: everything at or after t1 → 3 rows (2 in-window + 1 after).
218
+ const sinceT1 = await stack.http.queryOk<AuditResponse>(
219
+ AuditQueries.list,
220
+ { from: t1.toString() },
221
+ admin,
222
+ );
223
+ expect(sinceT1.rows).toHaveLength(3);
224
+
225
+ // To-only: everything at or before t1 → just the before-window row.
226
+ const untilT1 = await stack.http.queryOk<AuditResponse>(
227
+ AuditQueries.list,
228
+ { to: t1.toString() },
229
+ admin,
230
+ );
231
+ expect(untilT1.rows).toHaveLength(1);
232
+ expect((untilT1.rows[0]?.payload as { name?: string }).name).toBe("before-window");
233
+ });
234
+
235
+ test("rejects inverted from/to range with validation_error", async () => {
236
+ // from > to would silently return empty — confusing. Schema-level refine
237
+ // turns it into a clean 400 at the gate.
238
+ const res = await stack.http.query(
239
+ AuditQueries.list,
240
+ { from: "2030-01-01T00:00:00Z", to: "2020-01-01T00:00:00Z" },
241
+ admin,
242
+ );
243
+ expect(res.status).toBe(400);
244
+ });
245
+
246
+ test("rejects non-numeric cursor with validation_error (no PG crash path)", async () => {
247
+ // Pre-fix the handler interpolated `before` directly as bigint, so
248
+ // "abc" would raise an uncaught invalid_text_representation from PG.
249
+ // The schema regex catches it at the gate.
250
+ const res = await stack.http.query(AuditQueries.list, { before: "not-a-number" }, admin);
251
+ expect(res.status).toBe(400);
252
+ });
253
+
254
+ test("pagination: limit + nextBefore cursor walks the log", async () => {
255
+ for (let i = 0; i < 5; i++) {
256
+ await createWidget(admin, `W${i}`);
257
+ }
258
+
259
+ const page1 = await stack.http.queryOk<AuditResponse>(AuditQueries.list, { limit: 2 }, admin);
260
+ expect(page1.rows).toHaveLength(2);
261
+ expect(page1.nextBefore).not.toBeNull();
262
+
263
+ const page2 = await stack.http.queryOk<AuditResponse>(
264
+ AuditQueries.list,
265
+ { limit: 2, before: page1.nextBefore },
266
+ admin,
267
+ );
268
+ expect(page2.rows).toHaveLength(2);
269
+ const page1Ids = page1.rows.map((r) => r.id);
270
+ const page2Ids = page2.rows.map((r) => r.id);
271
+ for (const id of page2Ids) expect(page1Ids).not.toContain(id);
272
+
273
+ const page3 = await stack.http.queryOk<AuditResponse>(
274
+ AuditQueries.list,
275
+ { limit: 2, before: page2.nextBefore },
276
+ admin,
277
+ );
278
+ // 5 events, 2+2+1: final page is partial ⇒ nextBefore null.
279
+ expect(page3.rows).toHaveLength(1);
280
+ expect(page3.nextBefore).toBeNull();
281
+ });
282
+
283
+ test("response carries the full event payload + metadata (the audit-relevant detail)", async () => {
284
+ const id = await createWidget(admin, "Auditable", "green");
285
+ await stack.http.writeOk(
286
+ "widgets:write:widget:update",
287
+ { id, version: 1, changes: { color: "yellow" } },
288
+ admin,
289
+ );
290
+
291
+ const res = await stack.http.queryOk<AuditResponse>(
292
+ AuditQueries.list,
293
+ { aggregateId: id },
294
+ admin,
295
+ );
296
+ // Two events on this stream: created + updated. Newest first.
297
+ expect(res.rows).toHaveLength(2);
298
+ const [updated, created] = res.rows;
299
+
300
+ // created: payload IS the initial entity snapshot.
301
+ expect(created?.type).toBe("widget.created");
302
+ expect(created?.payload).toMatchObject({ name: "Auditable", color: "green" });
303
+ // metadata carries the actor (userId) for the write.
304
+ expect(created?.metadata).toMatchObject({ userId: admin.id });
305
+
306
+ // updated: payload is { changes, previous } — both halves matter for audit.
307
+ expect(updated?.type).toBe("widget.updated");
308
+ expect(updated?.payload).toMatchObject({
309
+ changes: { color: "yellow" },
310
+ previous: expect.objectContaining({ color: "green", name: "Auditable" }),
311
+ });
312
+ expect(updated?.metadata).toMatchObject({ userId: admin.id });
313
+ });
314
+
315
+ test("access denied for non-admin roles", async () => {
316
+ await createWidget(admin, "A");
317
+ // regularUser has role "User" — the handler requires Admin/SystemAdmin.
318
+ const res = await stack.http.query(AuditQueries.list, {}, regularUser);
319
+ expect(res.status).toBe(403);
320
+ const body = (await res.json()) as {
321
+ error?: { code?: string; details?: { reason?: string } };
322
+ };
323
+ // Pin the specific failure class. The framework raises AccessDeniedError
324
+ // with code=access_denied; asserting on `code` beats a status-only check
325
+ // (a 403 could also come from ownership-denied, for example).
326
+ expect(body.error?.code).toBe("access_denied");
327
+ });
328
+ });
@@ -0,0 +1,7 @@
1
+ // Qualified handler names — kept in one place so tests/clients can reference
2
+ // the audit query without hard-coding the full string.
3
+ export const AUDIT_FEATURE = "audit" as const;
4
+
5
+ export const AuditQueries = {
6
+ list: "audit:query:list",
7
+ } as const;
@@ -0,0 +1,23 @@
1
+ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { listQuery } from "./handlers/list.query";
3
+
4
+ // Audit feature — exposes a filtered read over the framework's event log.
5
+ //
6
+ // Design: the event-store IS the audit trail (every entity write produces
7
+ // an event with who/when/what/where/delta). This feature adds no persistence,
8
+ // no projection, no cursor — it's a single privileged query handler over
9
+ // the existing `events` table. See handlers/list.query.ts for the filter
10
+ // surface.
11
+ //
12
+ // Retention lives elsewhere. Events are kept indefinitely as the source of
13
+ // truth for state; archive or compress policies are a separate concern
14
+ // (tracked with the snapshot/archive infrastructure that already exists in
15
+ // the framework).
16
+ export function createAuditFeature(): FeatureDefinition {
17
+ return defineFeature("audit", (r) => {
18
+ const queries = {
19
+ list: r.queryHandler(listQuery),
20
+ };
21
+ return { queries };
22
+ });
23
+ }
@@ -0,0 +1,98 @@
1
+ // Audit query — reads the event-store's `events` table directly. The event-
2
+ // log IS the audit trail by construction: every entity write appends at least
3
+ // one event with createdBy (who), createdAt (when), tenantId (where),
4
+ // aggregateType + aggregateId (what), type (action), and payload (delta).
5
+ //
6
+ // No projection, no separate audit table. Queryable with the same filter
7
+ // surface any audit UI needs; tenant-isolated at the WHERE level so cross-
8
+ // tenant peeking is structurally impossible for non-SystemAdmin callers.
9
+ //
10
+ // Sensitive field-values are already stripped out of payloads at event-
11
+ // append time (see event-store-executor → stripSensitive), so this query
12
+ // can't surface PII that the entity definition marked as sensitive.
13
+
14
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
15
+ import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
16
+ import { and, desc, eq, gte, lt, lte } from "drizzle-orm";
17
+ import { z } from "zod";
18
+
19
+ // Per-page cap. 100 keeps a single page payload bounded while being enough
20
+ // for a humans-browse UI — clients that need exports iterate by `before`.
21
+ const MAX_LIMIT = 100;
22
+
23
+ export const listQuery = defineQueryHandler({
24
+ name: "list",
25
+ schema: z
26
+ .object({
27
+ // Cursor-style pagination: pass the `id` from the last row of the
28
+ // previous page as `before`. bigserial ids are monotonic, so `< before`
29
+ // reliably returns "the next older page". Beats OFFSET on large tables.
30
+ // The regex pins the input to digits-only — otherwise an invalid value
31
+ // would surface as a raw PG `invalid_text_representation` instead of a
32
+ // clean 400 at the schema gate.
33
+ before: z.string().regex(/^\d+$/, "cursor must be a positive integer").optional(),
34
+ limit: z.number().int().min(1).max(MAX_LIMIT).default(50),
35
+ // Filters — all optional. Combined via AND.
36
+ aggregateType: z.string().optional(),
37
+ aggregateId: z.uuid().optional(),
38
+ eventType: z.string().optional(),
39
+ // createdBy is stored as text on the events table (it accepts both UUIDs
40
+ // and system actor strings like "SYSTEM"), so the filter is a plain
41
+ // equality check on the raw value.
42
+ userId: z.string().optional(),
43
+ // Inclusive bounds. Clients pass ISO-8601; we parse to Temporal.Instant
44
+ // and compare via the `instant()` column type.
45
+ from: z.iso.datetime().optional(),
46
+ to: z.iso.datetime().optional(),
47
+ })
48
+ .refine((v) => !v.from || !v.to || v.from <= v.to, {
49
+ message: "`from` must be less than or equal to `to`",
50
+ path: ["from"],
51
+ }),
52
+ access: { roles: ["Admin", "SystemAdmin"] },
53
+ handler: async (query, ctx) => {
54
+ const p = query.payload;
55
+ const tenantId = query.user.tenantId;
56
+
57
+ const conditions = [eq(eventsTable.tenantId, tenantId)];
58
+ if (p.aggregateType) conditions.push(eq(eventsTable.aggregateType, p.aggregateType));
59
+ if (p.aggregateId) conditions.push(eq(eventsTable.aggregateId, p.aggregateId));
60
+ if (p.eventType) conditions.push(eq(eventsTable.type, p.eventType));
61
+ if (p.userId) conditions.push(eq(eventsTable.createdBy, p.userId));
62
+ if (p.from) conditions.push(gte(eventsTable.createdAt, Temporal.Instant.from(p.from)));
63
+ if (p.to) conditions.push(lte(eventsTable.createdAt, Temporal.Instant.from(p.to)));
64
+ // `before` = last seen id from the previous page. bigserial so `<` walks
65
+ // backwards in chronological order. Schema-regex guarantees the string
66
+ // is digits-only, so BigInt(...) can't throw.
67
+ if (p.before) conditions.push(lt(eventsTable.id, BigInt(p.before)));
68
+
69
+ const rows = await ctx.db
70
+ .select({
71
+ id: eventsTable.id,
72
+ aggregateId: eventsTable.aggregateId,
73
+ aggregateType: eventsTable.aggregateType,
74
+ version: eventsTable.version,
75
+ type: eventsTable.type,
76
+ payload: eventsTable.payload,
77
+ metadata: eventsTable.metadata,
78
+ createdAt: eventsTable.createdAt,
79
+ createdBy: eventsTable.createdBy,
80
+ })
81
+ .from(eventsTable)
82
+ .where(and(...conditions))
83
+ .orderBy(desc(eventsTable.id))
84
+ .limit(p.limit);
85
+
86
+ // bigint ids need serialisation — JSON can't carry a plain BigInt, and
87
+ // clients pass the cursor back as a string via `before`. Stringified once
88
+ // here so the response shape matches what the caller will re-submit.
89
+ const serialised = rows.map((r) => ({ ...r, id: String(r["id"]) }));
90
+ const last = serialised[serialised.length - 1];
91
+ return {
92
+ rows: serialised,
93
+ // Cursor for the NEXT page. Null when this page is partial (we hit
94
+ // the start of the log) so clients know to stop.
95
+ nextBefore: serialised.length === p.limit && last ? last["id"] : null,
96
+ };
97
+ },
98
+ });
@@ -0,0 +1,2 @@
1
+ export { AUDIT_FEATURE, AuditQueries } from "./constants";
2
+ export { createAuditFeature } from "./feature";