@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,39 @@
1
+ import type { DbRow } from "@cosmicdrift/kumiko-framework/db";
2
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
5
+ import { z } from "zod";
6
+
7
+ export const triggerWrite = defineWriteHandler({
8
+ name: "trigger",
9
+ schema: z.object({
10
+ jobName: z.string(),
11
+ payload: z.record(z.string(), z.unknown()).optional(),
12
+ }),
13
+ access: { roles: ["SystemAdmin"] },
14
+ handler: async (event, ctx) => {
15
+ const registry = ctx.registry;
16
+ // `jobRunner` is a dynamic context extension — not a core HandlerContext field.
17
+ const jobRunner = ctx["jobRunner"] as JobRunner;
18
+
19
+ const jobDef = registry.getJob(event.payload.jobName);
20
+ if (!jobDef) {
21
+ return writeFailure(
22
+ new NotFoundError("job", event.payload.jobName, {
23
+ i18nKey: "jobs.errors.unknownJob",
24
+ }),
25
+ );
26
+ }
27
+
28
+ const payload = (event.payload.payload ?? {}) as DbRow;
29
+ const bullJobId = await jobRunner.dispatch(event.payload.jobName, payload, {
30
+ triggeredById: event.user.id,
31
+ payload: JSON.stringify(payload),
32
+ });
33
+
34
+ return {
35
+ isSuccess: true,
36
+ data: { jobName: event.payload.jobName, bullJobId },
37
+ };
38
+ },
39
+ });
@@ -0,0 +1,5 @@
1
+ export { createJobsFeature } from "./feature";
2
+ export type { JobRunLoggerCallbacks } from "./job-run-logger";
3
+ export { createJobRunLogger } from "./job-run-logger";
4
+ export type { JobLogLevel, JobRunStatus } from "./job-run-table";
5
+ export { jobRunLogsTable, jobRunsTable } from "./job-run-table";
@@ -0,0 +1,213 @@
1
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import { type Registry, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { append, getStreamVersion } from "@cosmicdrift/kumiko-framework/event-store";
4
+ import type { JobLogEntry, JobMeta, JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
5
+ import { runProjectionsForEvent } from "@cosmicdrift/kumiko-framework/pipeline";
6
+ import { generateId } from "@cosmicdrift/kumiko-framework/utils";
7
+ import { eq } from "drizzle-orm";
8
+ import { runCompletedSchema, runFailedSchema, runStartedSchema } from "./events";
9
+ import { jobRunsTable } from "./job-run-table";
10
+
11
+ // ES job-run lifecycle:
12
+ // - onJobStart → jobs:event:run-started (first append, version 0→1)
13
+ // - onJobComplete → jobs:event:run-completed (append at current version,
14
+ // payload carries the batched logs)
15
+ // - onJobFailed → jobs:event:run-failed (same shape as completed + error)
16
+ //
17
+ // BullMQ callbacks don't carry a tenantId (jobs are cross-tenant). We
18
+ // anchor every run on SYSTEM_TENANT_ID — mirrors how config system-scope
19
+ // rows use the sentinel. The stream still works per-run because
20
+ // aggregate_id is a fresh UUID per run.
21
+
22
+ export const JOB_RUN_STARTED_EVENT = "jobs:event:run-started" as const;
23
+ export const JOB_RUN_COMPLETED_EVENT = "jobs:event:run-completed" as const;
24
+ export const JOB_RUN_FAILED_EVENT = "jobs:event:run-failed" as const;
25
+
26
+ export type JobRunLoggerOptions = {
27
+ readonly db: DbConnection;
28
+ readonly registry: Registry;
29
+ };
30
+
31
+ export type JobRunLoggerCallbacks = Pick<
32
+ JobRunnerOptions,
33
+ "onJobStart" | "onJobComplete" | "onJobFailed"
34
+ >;
35
+
36
+ // Default cap on the bullJobId → runId cache. A worker that starts jobs
37
+ // without ever seeing complete/failed callbacks (e.g. crashes mid-run)
38
+ // would otherwise leak entries indefinitely. 10k fits ~1 hour of
39
+ // high-throughput jobs; past that we evict oldest. DB-lookup recovers
40
+ // evicted entries, so correctness isn't at stake — only memory bounds.
41
+ const DEFAULT_CACHE_MAX_ENTRIES = 10_000;
42
+ // Entry TTL. A run that hangs longer than this is either a real stuck
43
+ // worker (ops should alert) or a test-environment run that never fired
44
+ // complete/failed; either way the cache entry has no value. Falls back
45
+ // to DB-lookup if actually needed.
46
+ const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
47
+
48
+ export function createJobRunLogger(opts: JobRunLoggerOptions): JobRunLoggerCallbacks {
49
+ const { db, registry } = opts;
50
+
51
+ // bullJobId → aggregate uuid. BullMQ hands us the bullJobId on every
52
+ // callback, but our aggregate stream is keyed by a fresh UUID we mint
53
+ // on start. The cache threads that UUID from onJobStart through to
54
+ // onJobComplete/onJobFailed so the completion-event lands on the same
55
+ // stream as the start-event.
56
+ //
57
+ // Bounded cache (LRU-ish with TTL) — worker-crash between start and
58
+ // complete would otherwise leak entries. DB-lookup recovers evicted
59
+ // entries via bull_job_id on the projection.
60
+ type CacheEntry = { readonly runId: string; readonly expiresAt: number };
61
+ const runIdByBullJobId = new Map<string, CacheEntry>();
62
+
63
+ function cachePut(bullJobId: string, runId: string): void {
64
+ // Enforce max-size BEFORE insert. Map iteration returns insertion
65
+ // order, so dropping the first entry is the oldest.
66
+ if (runIdByBullJobId.size >= DEFAULT_CACHE_MAX_ENTRIES) {
67
+ const oldest = runIdByBullJobId.keys().next().value;
68
+ if (oldest !== undefined) runIdByBullJobId.delete(oldest);
69
+ }
70
+ runIdByBullJobId.set(bullJobId, {
71
+ runId,
72
+ expiresAt: Date.now() + DEFAULT_CACHE_TTL_MS,
73
+ });
74
+ }
75
+
76
+ function cacheGet(bullJobId: string): string | undefined {
77
+ const entry = runIdByBullJobId.get(bullJobId);
78
+ if (!entry) return undefined;
79
+ if (Date.now() >= entry.expiresAt) {
80
+ runIdByBullJobId.delete(bullJobId); // immediate cleanup on terminal callback
81
+ return undefined;
82
+ }
83
+ return entry.runId;
84
+ }
85
+
86
+ async function resolveRunId(bullJobId: string): Promise<string | undefined> {
87
+ const cached = cacheGet(bullJobId);
88
+ if (cached) return cached;
89
+ const [row] = await db
90
+ .select({ id: jobRunsTable.id })
91
+ .from(jobRunsTable)
92
+ .where(eq(jobRunsTable.bullJobId, bullJobId));
93
+ // buildBaseColumns's signature types `id` as `string | number` because
94
+ // it returns both branches of the idType union. We know this table
95
+ // was built with idType: "uuid" (see job-run-table.ts), so narrowing
96
+ // via String() is safe runtime-wise. A proper framework-level fix
97
+ // would overload buildBaseColumns per idType — scoped out of this
98
+ // follow-up as its return type has four branches (with/without
99
+ // softDelete × serial/uuid).
100
+ const id = row ? String(row.id) : undefined;
101
+ if (id) cachePut(bullJobId, id);
102
+ return id;
103
+ }
104
+
105
+ return {
106
+ onJobStart: async (jobName: string, bullJobId: string, meta: JobMeta) => {
107
+ const runId = generateId();
108
+ cachePut(bullJobId, runId);
109
+ // Parse against the registered schema so out-of-dispatcher writes
110
+ // get the same validation guarantee as ctx.appendEvent. A shape
111
+ // drift between feature + logger fails loudly at the source
112
+ // instead of silently landing on the events-table.
113
+ const payload = runStartedSchema.parse({
114
+ jobName,
115
+ bullJobId,
116
+ status: "running",
117
+ payload: meta.payload ?? null,
118
+ triggeredById: meta.triggeredById ?? null,
119
+ startedAt: Temporal.Now.instant().toString(),
120
+ attempt: meta.attempt ?? 1,
121
+ });
122
+ const event = await append(db, {
123
+ aggregateId: runId,
124
+ aggregateType: "jobRun",
125
+ tenantId: SYSTEM_TENANT_ID,
126
+ expectedVersion: 0,
127
+ type: JOB_RUN_STARTED_EVENT,
128
+ payload,
129
+ metadata: { userId: "system" },
130
+ });
131
+ await runProjectionsForEvent(event, registry, db);
132
+ },
133
+
134
+ onJobComplete: async (
135
+ _jobName: string,
136
+ bullJobId: string,
137
+ duration: number,
138
+ logs: JobLogEntry[],
139
+ ) => {
140
+ const runId = await resolveRunId(bullJobId);
141
+ // skip: state loss between start + complete (worker restart, cache
142
+ // evicted AND DB has no matching bull_job_id). Rare edge case; we
143
+ // drop the completion event rather than forging a jobRun aggregate
144
+ // from scratch — forensics still has the original BullMQ lifecycle.
145
+ if (!runId) return;
146
+ const currentVersion = await getStreamVersion(db, runId, SYSTEM_TENANT_ID);
147
+ const payload = runCompletedSchema.parse({
148
+ duration,
149
+ finishedAt: Temporal.Now.instant().toString(),
150
+ logs: logs.map((l) => ({
151
+ level: l.level,
152
+ message: l.message,
153
+ timestamp: l.timestamp.toString(),
154
+ })),
155
+ });
156
+ const event = await append(db, {
157
+ aggregateId: runId,
158
+ aggregateType: "jobRun",
159
+ tenantId: SYSTEM_TENANT_ID,
160
+ expectedVersion: currentVersion,
161
+ type: JOB_RUN_COMPLETED_EVENT,
162
+ payload,
163
+ metadata: { userId: "system" },
164
+ });
165
+ await runProjectionsForEvent(event, registry, db);
166
+ runIdByBullJobId.delete(bullJobId); // immediate cleanup on terminal callback
167
+ },
168
+
169
+ onJobFailed: async (
170
+ _jobName: string,
171
+ bullJobId: string,
172
+ error: string,
173
+ logs: JobLogEntry[],
174
+ ) => {
175
+ const runId = await resolveRunId(bullJobId);
176
+ // skip: same rare state-loss case as in onJobComplete — drop the
177
+ // failure event rather than forge a jobRun aggregate from scratch.
178
+ if (!runId) return;
179
+ const currentVersion = await getStreamVersion(db, runId, SYSTEM_TENANT_ID);
180
+ // Read started_at off the projection so we can compute duration
181
+ // symmetrically to onJobComplete (which gets duration from the
182
+ // worker). The projection already has started_at from the
183
+ // run-started inline-apply.
184
+ const [row] = await db
185
+ .select({ startedAt: jobRunsTable.startedAt })
186
+ .from(jobRunsTable)
187
+ .where(eq(jobRunsTable.id, runId));
188
+ const now = Temporal.Now.instant();
189
+ const duration = row ? Number(now.since(row.startedAt).total({ unit: "millisecond" })) : 0;
190
+ const payload = runFailedSchema.parse({
191
+ duration,
192
+ finishedAt: now.toString(),
193
+ error,
194
+ logs: logs.map((l) => ({
195
+ level: l.level,
196
+ message: l.message,
197
+ timestamp: l.timestamp.toString(),
198
+ })),
199
+ });
200
+ const event = await append(db, {
201
+ aggregateId: runId,
202
+ aggregateType: "jobRun",
203
+ tenantId: SYSTEM_TENANT_ID,
204
+ expectedVersion: currentVersion,
205
+ type: JOB_RUN_FAILED_EVENT,
206
+ payload,
207
+ metadata: { userId: "system" },
208
+ });
209
+ await runProjectionsForEvent(event, registry, db);
210
+ runIdByBullJobId.delete(bullJobId); // immediate cleanup on terminal callback
211
+ },
212
+ };
213
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ buildBaseColumns,
3
+ instant,
4
+ integer,
5
+ table as pgTable,
6
+ serial,
7
+ text,
8
+ } from "@cosmicdrift/kumiko-framework/db";
9
+
10
+ export type JobRunStatus = "queued" | "running" | "completed" | "failed";
11
+ export type JobLogLevel = "info" | "warn" | "error";
12
+
13
+ // jobRun is a system-scoped events-only aggregate: every job execution is
14
+ // its own stream, driven entirely by BullMQ-callbacks (onJobStart /
15
+ // -Complete / -Failed) via the low-level append() path. Three domain-
16
+ // events cover the lifecycle:
17
+ // - `jobs:event:run-started` (when BullMQ picks a job off its queue)
18
+ // - `jobs:event:run-completed` (duration + batched log entries)
19
+ // - `jobs:event:run-failed` (error + duration + batched log entries)
20
+ //
21
+ // Logs ride the completed/failed event as an array — "Option B" from the
22
+ // design discussion: one event per run instead of N events per log line,
23
+ // no log duplication across status transitions. The inline projection
24
+ // expands the batch into N rows in jobRunLogsTable, keeping the pre-ES
25
+ // detail-query-shape intact.
26
+ //
27
+ // No r.entity is registered for `jobRun` — the boot-validator accepts
28
+ // events-only projection sources where every apply-key is a registered
29
+ // domain-event (see registry.ts).
30
+ export const jobRunsTable = pgTable("read_job_runs", {
31
+ ...buildBaseColumns(false, "uuid"),
32
+ jobName: text("job_name").notNull(),
33
+ bullJobId: text("bull_job_id").notNull(),
34
+ status: text("status").notNull().$type<JobRunStatus>(),
35
+ payload: text("payload"),
36
+ error: text("error"),
37
+ attempt: integer("attempt").default(1).notNull(),
38
+ startedAt: instant("started_at").notNull(),
39
+ finishedAt: instant("finished_at"),
40
+ duration: integer("duration"),
41
+ triggeredById: text("triggered_by_id"),
42
+ });
43
+
44
+ // Child projection keyed by the jobRun aggregate id. Pre-ES used a serial
45
+ // PK + integer runId; post-ES runId is still exposed but now holds the
46
+ // uuid of the parent jobRun. Existing detail-query callers treat it as an
47
+ // opaque identifier, so the type-switch is backward-compatible at the
48
+ // query surface.
49
+ export const jobRunLogsTable = pgTable("read_job_run_logs", {
50
+ id: serial("id").primaryKey(),
51
+ runId: text("run_id").notNull(),
52
+ level: text("level").notNull().$type<JobLogLevel>(),
53
+ message: text("message").notNull(),
54
+ timestamp: instant("timestamp").notNull(),
55
+ });
@@ -0,0 +1,195 @@
1
+ # legal-pages
2
+
3
+ Opt-in wrapper around [`text-content`](../text-content/) for
4
+ DACH compliance. Ships four fixed public HTML routes
5
+ (`/legal/impressum`, `/legal/datenschutz`, `/legal/imprint`,
6
+ `/legal/privacy`) with Markdown→HTML rendering and a boot check that
7
+ hard-fails in production when the DE required blocks aren't seeded.
8
+
9
+ **Opt-in.** Internal tools, US apps without an imprint requirement,
10
+ or hobby projects without public access simply don't activate the
11
+ feature.
12
+
13
+ ---
14
+
15
+ ## Setup
16
+
17
+ ```typescript
18
+ import { createLegalPagesFeature } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
19
+ import {
20
+ createTextContentApi,
21
+ createTextContentFeature,
22
+ } from "@cosmicdrift/kumiko-bundled-features/text-content";
23
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
24
+
25
+ runProdApp({
26
+ features: [
27
+ createTextContentFeature(), // legal-pages requires text-content
28
+ createLegalPagesFeature(),
29
+ /* ... */
30
+ ],
31
+ // Two wirings are required:
32
+ // 1. anonymousAccess for /legal/* routes (run without a JWT)
33
+ // 2. extraContext.textContent for the boot check (cross-feature
34
+ // decoupling — legal-pages imports no code from text-content,
35
+ // only uses the API via ctx)
36
+ anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
37
+ extraContext: ({ db }) => ({
38
+ textContent: createTextContentApi(db),
39
+ }),
40
+ });
41
+ ```
42
+
43
+ ---
44
+
45
+ ### Production table setup
46
+
47
+ legal-pages doesn't have its own table — it uses text-content's
48
+ `read_text_blocks`. Table setup therefore goes through text-content:
49
+
50
+ ```bash
51
+ yarn kumiko migrate generate # text-block entity is detected
52
+ yarn kumiko migrate apply
53
+ ```
54
+
55
+ See [text-content/README.md](../text-content/README.md#production-table-setup).
56
+
57
+ ## Routes
58
+
59
+ | Path | Slug + lang | Title fallback (when block empty) |
60
+ |---|---|---|
61
+ | `GET /legal/impressum` | `imprint` / `de` | "Impressum" |
62
+ | `GET /legal/datenschutz` | `privacy` / `de` | "Datenschutzerklärung" |
63
+ | `GET /legal/imprint` | `imprint` / `en` | "Imprint" |
64
+ | `GET /legal/privacy` | `privacy` / `en` | "Privacy Policy" |
65
+
66
+ Response:
67
+ - `200 text/html` — block exists + has body. Cache header `public, max-age=300`.
68
+ - `404 text/plain` — block missing. Hint: "Tenant admin must set this text block".
69
+ - `503 text/plain` — `app.fetch` to `/api/query` failed (anonymousAccess missing?).
70
+
71
+ Layout: a minimal HTML5 skeleton with inline CSS — apps that want to
72
+ integrate into their own layout use `text-content:query:by-slug`
73
+ directly and render themselves.
74
+
75
+ ---
76
+
77
+ ## Boot check
78
+
79
+ `r.job` with `runOnBoot: true` checks at app start whether the DE
80
+ required blocks exist in SYSTEM_TENANT:
81
+
82
+ | Slug + lang | What happens when missing |
83
+ |---|---|
84
+ | `imprint` / `de` | **Production:** `throw new Error(...)` blocks app start. **Dev:** `ctx.log.warn(...)` |
85
+ | `privacy` / `de` | as above |
86
+
87
+ EN versions are **not** boot-fail-relevant (`LEGAL_OPTIONAL_BLOCKS`).
88
+ Routes return `404` if an EN block is missing.
89
+
90
+ → Apps that activate the feature must seed both DE blocks before a
91
+ production deploy — either via a bootstrap script (`seedTextBlock`) or
92
+ manually via the TenantAdmin API.
93
+
94
+ ---
95
+
96
+ ## TenantAdmin maintenance via the API
97
+
98
+ Tenant admins (or platform SystemAdmin for SYSTEM_TENANT texts) can
99
+ update content at any time through the standard write handler:
100
+
101
+ ```typescript
102
+ // From the tenant admin frontend (or admin curl):
103
+ await fetch("/api/write", {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` },
106
+ body: JSON.stringify({
107
+ type: "text-content:write:set",
108
+ payload: {
109
+ slug: "imprint",
110
+ lang: "de",
111
+ title: "Impressum",
112
+ body: "## Angaben gemäß § 5 TMG\n\n...",
113
+ },
114
+ }),
115
+ });
116
+ ```
117
+
118
+ → Idempotent: a second call with the same `(slug, lang)` updates the block.
119
+ ACL: `roles: ["TenantAdmin", "SystemAdmin"]` — SystemAdmin (a global
120
+ role) may set SYSTEM_TENANT texts, TenantAdmin only tenant-owned ones.
121
+
122
+ → The route's cache header is `public, max-age=300` — after an update,
123
+ visitors see new content within 5 minutes at most. If you need
124
+ instant visibility, you can help things along with a CDN purge.
125
+
126
+ ## Seeding
127
+
128
+ On first app boot or via migration:
129
+
130
+ ```typescript
131
+ import { seedTextBlock } from "@cosmicdrift/kumiko-bundled-features/text-content/seeding";
132
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
133
+
134
+ await seedTextBlock(db, {
135
+ tenantId: SYSTEM_TENANT_ID,
136
+ slug: "imprint",
137
+ lang: "de",
138
+ title: "Impressum",
139
+ body: `## Angaben gemäß § 5 TMG
140
+
141
+ **Marc Frost**
142
+
143
+ Slevogtstr. 10
144
+ 04159 Leipzig
145
+
146
+ ## Kontakt
147
+
148
+ E-Mail: hello@example.com`,
149
+ });
150
+ ```
151
+
152
+ Templates for imprint + privacy policy: see
153
+ [docs/plans/datenschutz/legal-artifacts.md](../../../../docs/plans/datenschutz/legal-artifacts.md)
154
+ and vetted external generators (e-recht24.de,
155
+ datenschutz-generator.de).
156
+
157
+ ---
158
+
159
+ ## XSS — currently not secured by design
160
+
161
+ `marked` renders HTML tags 1:1, so a malicious tenant admin could in
162
+ theory put `<script>` into the body.
163
+
164
+ Currently accepted because:
165
+ - only `roles: ["TenantAdmin"]` may set texts
166
+ - multi-author setups don't exist yet
167
+ - self-hosted tier without unknown tenant admins
168
+
169
+ **Phase-2 hardening:** `DOMPurify` or `isomorphic-dompurify`
170
+ sanitization step between `marked.parse()` and the response.
171
+ Documented when a customer with a multi-author setup shows up.
172
+
173
+ ---
174
+
175
+ ## Tenant model
176
+
177
+ **1 app = X tenants = 1 imprint.** All subdomains/tenant hosts of a
178
+ Kumiko app share the SYSTEM_TENANT version of the legal pages. If you
179
+ need per-tenant imprints (rare — typical case: the platform operator
180
+ is the responsible party, not the tenant customer), call
181
+ text-content's by-slug query directly with a tenant-specific TenantId
182
+ and put your own routes in front.
183
+
184
+ ---
185
+
186
+ ## Architecture cross-refs
187
+
188
+ - [docs/plans/datenschutz/](../../../../docs/plans/datenschutz/)
189
+ — consolidated privacy plan index
190
+ - [docs/plans/datenschutz/legal-artifacts.md](../../../../docs/plans/datenschutz/legal-artifacts.md)
191
+ — templates + where-is-what for imprint/AVV/TOMs/RoPA
192
+ - [docs/plans/datenschutz/compliance-as-product.md](../../../../docs/plans/datenschutz/compliance-as-product.md)
193
+ — roadmap for auto-generation (sub-processor list, TOMs, data-breach workflow)
194
+ - [samples/recipes/legal-pages/](../../../../samples/recipes/legal-pages/)
195
+ — live sample with both features wired up