@cosmicdrift/kumiko-samples 0.387.0 → 0.389.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 (274) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +8 -1
  3. package/packages/bundled-features/src/agent-tools/changes.json +12 -0
  4. package/packages/bundled-features/src/agent-tools/index.ts +1 -1
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
  6. package/packages/bundled-features/src/audit/changes.json +12 -0
  7. package/packages/bundled-features/src/audit/feature.ts +1 -0
  8. package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
  10. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
  11. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
  12. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
  13. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
  14. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
  15. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
  16. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
  17. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
  18. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
  19. package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
  20. package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
  21. package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
  22. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
  23. package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
  24. package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
  25. package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
  26. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
  27. package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
  28. package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
  29. package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
  30. package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
  32. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
  33. package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
  34. package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
  35. package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
  36. package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
  37. package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
  38. package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
  39. package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
  40. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
  51. package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
  52. package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
  53. package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
  55. package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
  57. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
  58. package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
  59. package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
  60. package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
  61. package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
  62. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
  63. package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
  64. package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
  65. package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
  66. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
  67. package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
  68. package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
  69. package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
  70. package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
  71. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
  72. package/packages/bundled-features/src/cap-counter/changes.json +30 -0
  73. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
  74. package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
  75. package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
  76. package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
  77. package/packages/bundled-features/src/channel-email/changes.json +6 -0
  78. package/packages/bundled-features/src/channel-email/feature.ts +1 -1
  79. package/packages/bundled-features/src/config/changes.json +18 -0
  80. package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
  81. package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
  82. package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
  83. package/packages/bundled-features/src/config/write-helpers.ts +27 -8
  84. package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
  85. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
  86. package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
  87. package/packages/bundled-features/src/delivery/changes.json +42 -0
  88. package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
  89. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
  90. package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
  91. package/packages/bundled-features/src/delivery/feature.ts +1 -0
  92. package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
  93. package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
  94. package/packages/bundled-features/src/delivery/index.ts +5 -0
  95. package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
  96. package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
  97. package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
  98. package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
  99. package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
  100. package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
  101. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  102. package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
  103. package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
  104. package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
  105. package/packages/bundled-features/src/file-foundation/index.ts +1 -0
  106. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
  107. package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
  108. package/packages/bundled-features/src/files/README.md +1 -1
  109. package/packages/bundled-features/src/form-draft/changes.json +6 -0
  110. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
  111. package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
  112. package/packages/bundled-features/src/host-routing/README.md +1 -1
  113. package/packages/bundled-features/src/host-routing/changes.json +12 -0
  114. package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
  115. package/packages/bundled-features/src/host-routing/options.ts +2 -1
  116. package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
  118. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
  120. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
  122. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
  123. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
  124. package/packages/bundled-features/src/jobs/changes.json +13 -0
  125. package/packages/bundled-features/src/jobs/feature.ts +15 -4
  126. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
  127. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
  128. package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
  129. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  130. package/packages/bundled-features/src/legal-pages/README.md +1 -1
  131. package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
  132. package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
  133. package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
  134. package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
  135. package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
  136. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
  137. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  138. package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
  139. package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
  140. package/packages/bundled-features/src/managed-pages/index.ts +1 -0
  141. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
  142. package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
  143. package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
  144. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
  145. package/packages/bundled-features/src/secrets/changes.json +6 -0
  146. package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
  147. package/packages/bundled-features/src/sessions/changes.json +7 -0
  148. package/packages/bundled-features/src/sessions/feature.ts +8 -2
  149. package/packages/bundled-features/src/sessions/index.ts +1 -1
  150. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
  151. package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
  152. package/packages/bundled-features/src/shared/index.ts +1 -1
  153. package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
  154. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
  155. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
  156. package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
  157. package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
  158. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
  159. package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
  160. package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
  161. package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
  162. package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
  163. package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
  164. package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
  165. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
  166. package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
  167. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
  168. package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
  169. package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
  170. package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
  171. package/packages/bundled-features/src/tags/feature.ts +1 -0
  172. package/packages/bundled-features/src/template-resolver/api.ts +31 -4
  173. package/packages/bundled-features/src/template-resolver/changes.json +84 -0
  174. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
  175. package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
  176. package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
  177. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
  178. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
  179. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
  180. package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
  181. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
  182. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
  183. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
  184. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
  185. package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
  186. package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
  187. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
  188. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
  189. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
  190. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
  191. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
  192. package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
  193. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
  194. package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
  195. package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
  196. package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
  197. package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
  198. package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
  199. package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
  200. package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
  201. package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
  202. package/packages/bundled-features/src/tenant/changes.json +6 -0
  203. package/packages/bundled-features/src/tenant/feature.ts +1 -0
  204. package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
  205. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
  206. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
  207. package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
  208. package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
  209. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
  210. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
  211. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  212. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
  213. package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
  214. package/packages/bundled-features/src/user/feature.ts +1 -0
  215. package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
  216. package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
  217. package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
  218. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
  219. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
  220. package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
  221. package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
  222. package/packages/bundled-features/src/waitlist/changes.json +36 -0
  223. package/packages/bundled-features/src/waitlist/feature.ts +1 -0
  224. package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
  225. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
  226. package/packages/bundled-features/src/waitlist/import.ts +28 -3
  227. package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
  228. package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
  229. package/packages/bundled-features/src/waitlist/options.ts +5 -1
  230. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
  231. package/samples/apps/styleguide/src/app/client.tsx +2 -0
  232. package/samples/apps/styleguide/src/app/seed.ts +6 -4
  233. package/samples/apps/styleguide/src/app/server.ts +4 -1
  234. package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
  235. package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
  236. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
  237. package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
  238. package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
  239. package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
  240. package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
  241. package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
  242. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
  243. package/samples/apps/use-all-bundled/bin/main.ts +2 -0
  244. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
  245. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
  246. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
  247. package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
  248. package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
  249. package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
  250. package/samples/apps/use-all-bundled/package.json +2 -1
  251. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
  252. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
  253. package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
  254. package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
  255. package/samples/recipes/auth-mfa/README.md +5 -0
  256. package/samples/recipes/auth-mfa/package.json +5 -2
  257. package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
  258. package/samples/recipes/auth-mfa/tsconfig.json +8 -0
  259. package/samples/recipes/env-composition/README.md +3 -1
  260. package/samples/recipes/env-composition/src/env.ts +2 -0
  261. package/samples/recipes/golden-path/src/feature.ts +2 -1
  262. package/samples/recipes/legal-pages/README.md +1 -1
  263. package/samples/recipes/lifecycle-hooks/README.md +4 -0
  264. package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
  265. package/samples/recipes/list-table-options/README.md +50 -0
  266. package/samples/recipes/list-table-options/package.json +11 -0
  267. package/samples/recipes/list-table-options/src/feature.ts +221 -0
  268. package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
  269. package/samples/recipes/rate-limiting/README.md +18 -0
  270. package/samples/recipes/rate-limiting/src/feature.ts +12 -0
  271. package/samples/recipes/state-machine/src/events.ts +3 -2
  272. package/samples/recipes/tenant-isolation/README.md +1 -0
  273. package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
  274. package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
@@ -20,6 +20,7 @@ import { GALLERY_TENANT_HOST, GALLERY_TERMS_SLUG } from "./gallery-constants";
20
20
  import {
21
21
  createGalleryBillingFeature,
22
22
  createGalleryEmailFeature,
23
+ createGalleryMorEnableRoute,
23
24
  galleryMailTransport,
24
25
  galleryProviderFeature,
25
26
  galleryScreensFeature,
@@ -29,14 +30,22 @@ const port = Number.parseInt(process.env["PORT"] ?? "4196", 10);
29
30
 
30
31
  const BILLING_FEATURE = "billing-foundation";
31
32
  const EMAIL_FEATURE = "channel-email";
33
+ // The stock app mounts the real Mollie and Dodo providers; the gallery's
34
+ // provider choice is the two mocks, which never call out.
35
+ const REAL_PROVIDER_FEATURES: ReadonlySet<string> = new Set([
36
+ "subscription-mollie",
37
+ "subscription-dodo",
38
+ ]);
32
39
 
33
40
  const features = [
34
- ...devAppOptions.features.map((feature) => {
35
- if (feature.name === BILLING_FEATURE)
36
- return createGalleryBillingFeature(`http://localhost:${port}`);
37
- if (feature.name === EMAIL_FEATURE) return createGalleryEmailFeature();
38
- return feature;
39
- }),
41
+ ...devAppOptions.features
42
+ .filter((feature) => !REAL_PROVIDER_FEATURES.has(feature.name))
43
+ .map((feature) => {
44
+ if (feature.name === BILLING_FEATURE)
45
+ return createGalleryBillingFeature(`http://localhost:${port}`);
46
+ if (feature.name === EMAIL_FEATURE) return createGalleryEmailFeature();
47
+ return feature;
48
+ }),
40
49
  galleryProviderFeature,
41
50
  galleryScreensFeature,
42
51
  ];
@@ -77,6 +86,7 @@ await runDevApp({
77
86
  extraRoutes: [
78
87
  ...createE2eSeedRoutes({ mailOutbox: galleryMailTransport }),
79
88
  createSubscriptionWebhookRoute(),
89
+ createGalleryMorEnableRoute(),
80
90
  ...createContractTerminationRoutes(),
81
91
  ],
82
92
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "samples/apps/use-all-bundled APP_FEATURES (composeFeatures includeBundled + signup)",
3
- "featureCount": 87,
3
+ "featureCount": 88,
4
4
  "features": [
5
5
  {
6
6
  "name": "account-security",
@@ -71,6 +71,7 @@
71
71
  "name": "audit",
72
72
  "description": "Exposes the framework's event store as a paginated, filterable audit log via the `audit:query:list` handler (accessible to `Admin` and `SystemAdmin` roles). No separate table or projection — the event store is the audit trail by construction: every entity write already records who, when, what entity, and the event payload with PII stripped. A SystemAdmin can pass `scope: \"system\"` to read the app-instance system events such as `app.started`, and reads the events of every tenant (system tenant excluded) through `audit:query:list-all-tenants` and the `audit-log-all-tenants` screen. Filter by `aggregateType`, `aggregateId`, `eventType`, `userId`, or time range. Also records `audit:event:escape-hatch-used` whenever a handler uses one of the framework's escape hatches (unsafeRaw, acknowledgeCrossTenant, db.global() writes, or a granted identity switch).",
73
73
  "toggleableDefault": null,
74
+ "adoptsBundledLists": true,
74
75
  "requires": [
75
76
  "tenant",
76
77
  "user",
@@ -232,6 +233,7 @@
232
233
  "validated": false,
233
234
  "options": [
234
235
  "optional",
236
+ "systemAdmins",
235
237
  "admins",
236
238
  "all"
237
239
  ],
@@ -300,7 +302,7 @@
300
302
  },
301
303
  {
302
304
  "name": "billing-foundation",
303
- "description": "Plugin host for subscription billing — manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. `createBillingFoundationFeature({ baseUrl, catalog })` additionally derives a `billing-foundation:query:billing-plans` query, `start-plan-checkout`/`switch-plan` write-handlers and a dormant billing-plans dashboard screen/panel from the catalog. Also ships `billing-foundation:write:sync-subscription` (pulls a provider plugin's live subscription state via `retrieveSubscription` and appends drift as a `subscription.updated` event — catches changes made on the provider's own dashboard that never reached us as a webhook) and the `sync-subscriptions` job (manual-trigger + runOnBoot, perTenant) that dispatches it. Low-level building block — use `subscription-stripe` or `subscription-mollie` unless you are writing a new payment provider.",
305
+ "description": "Plugin host for subscription billing. Manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. `createBillingFoundationFeature({ baseUrl, catalog })` additionally derives a `billing-foundation:query:billing-plans` query, `start-plan-checkout`/`switch-plan` write-handlers, a `plan-switch-preview` query and a dormant billing-plans dashboard screen/panel from the catalog. Several providers can be mounted at once: new checkouts are offered by every enabled provider (enabled means keys set and billingLive on, or simply mounted for plugins without `isBillingEnabled`), the customer picks one in the plan panel and `start-plan-checkout` takes a `providerName`. An existing subscription stays with the provider that stored it, so the plan list and every switch follow that provider. A plan switch is either hosted (`createPlanSwitchSession` answers a url) or in-app (`previewPlanSwitch` shows the proration, `applyPlanSwitch` runs after the customer confirms; `switch-plan` then answers `{ applied: true }` and records the consent). `consumerProtection.providerTexts` overrides the checkout note and the VAT note per provider. Also ships `billing-foundation:write:sync-subscription` (pulls a provider plugin's live subscription state via `retrieveSubscription` and appends drift as a `subscription.updated` event, which catches changes made on the provider's own dashboard that never reached us as a webhook) and the `sync-subscriptions` job (manual-trigger + runOnBoot, perTenant) that dispatches it. Low-level building block: use `subscription-stripe`, `subscription-mollie` or `subscription-dodo` unless you are writing a new payment provider.",
304
306
  "toggleableDefault": null,
305
307
  "requires": [
306
308
  "tenant-lifecycle",
@@ -339,6 +341,7 @@
339
341
  "name": "cap-counter",
340
342
  "description": "Tracks per-tenant usage against configurable limits using two complementary storage models: calendar-period counters (one projection row per tenant/capName/period, reset implicitly by period rollover) and rolling-window counters (append-only event stream, no projection). Use `enforceCap` / `enforceRollingCap` (or the `withCapEnforcement` / `withRollingCapEnforcement` handler wrappers) in your write-handlers to check limits with soft-warn and hard-block tolerances; call `enforceCapAndMaybeNotify` when you also want to trigger a delivery notification on soft-threshold hits.",
341
343
  "toggleableDefault": null,
344
+ "adoptsBundledLists": true,
342
345
  "requires": [],
343
346
  "optionalRequires": [],
344
347
  "configReads": [],
@@ -362,6 +365,7 @@
362
365
  "name": "cap-overview",
363
366
  "description": "Read-only visibility into per-tenant tier assignment and cap usage. SystemAdmin gets a platform-wide tenant list with usage bars; tenant admins (or the roles in `usageVisibleTo`) get their own tenant's usage as dashboard cards. Reads tier-engine, billing-foundation, and tenant data plus app-owned usage tables via caller-supplied CapSpec callbacks — never writes.",
364
367
  "toggleableDefault": null,
368
+ "adoptsBundledLists": true,
365
369
  "requires": [
366
370
  "tenant",
367
371
  "tier-engine",
@@ -424,7 +428,7 @@
424
428
  },
425
429
  {
426
430
  "name": "channel-email",
427
- "description": "Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send the notifications declared with `tenantTransport: true` through the sending tenant's own configured provider: a broken provider fails the send, a tenant without one uses the app `transport`; every other notification (password reset, sign-in) always goes through the app `transport`. `transport` may be left out when `tenantTransport` is set; a mail that then has no transport fails with `missing_credentials`. Notifications declared with `unsubscribe: true` get a signed `data.unsubscribeUrl` from delivery, which turns into the `List-Unsubscribe` headers.",
431
+ "description": "Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send the notifications declared with `tenantTransport: true` through the sending tenant's own configured provider: a broken provider fails the send, a tenant without one uses the app `transport`; every other notification (password reset, sign-in) always goes through the app `transport`. `transport` may be left out when `tenantTransport` is set, but then only notifications declared with `tenantTransport: true` are delivered: every other notification (password reset, sign-in, invitations) fails with `missing_credentials`, so an app without an app SMTP server must mark every notification that should go out or keep an app `transport`. Notifications declared with `unsubscribe: true` get a signed `data.unsubscribeUrl` from delivery, which turns into the `List-Unsubscribe` headers.",
428
432
  "toggleableDefault": null,
429
433
  "requires": [
430
434
  "delivery"
@@ -744,6 +748,7 @@
744
748
  "name": "delivery",
745
749
  "description": "The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). Inside a write handler the delivery runs after the write has committed (the call returns `{ kind: \"deferred\" }`, a rolled-back write sends nothing); `immediate: true` delivers synchronously and returns `{ kind: \"dispatched\", deliveries }`. It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting — add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app's `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message's `data.unsubscribeUrl` points at the unsubscribe route. A notification declared with `unsubscribe: true` gets that link from delivery itself when the service has `unsubscribeLinks` (the `runProdApp` and `runDevApp` option `unsubscribe: { secret }` mounts the routes and wires it; base URL from `auth.publicOrigin`, `auth.mail.baseUrl` or `APP_BASE_URL`): user recipients get a user token, `route` recipients an address token (needs a blind-index key, otherwise no link); a `data.unsubscribeUrl` from the caller wins. `unsubscribe.tenantOrigin: (ctx) => origin | null` points the link at the tenant's own host (subdomain or custom domain, must serve this app's `/api`); null, a throw or a non-https value fall back to the app origin. The token carries the mail's locale, so the confirmation and result pages render in the mail's language. An app serving its own (e.g. legacy) unsubscribe links renders the same result page with `renderUnsubscribeResultPage({ kind, locale })`. To react to a no-account opt-out (e.g. remove the app's own subscriber row), declare `r.job(name, { trigger: { on: DeliveryHandlers.unsubscribeAddress } }, handler)` on a lane the app consumes: it runs after each committed opt-out, also an idempotent repeat, with `unsubscribeAddressPayloadSchema` (`addressHash`, `notificationType`, `channel`) as payload and the tenant in `ctx.triggeredBy.tenantId`; match the subscriber via `hashUnsubscribeAddress(email)`. Declare `unsubscribeUrl` as an optional template variable if the mail body shows it.",
746
750
  "toggleableDefault": null,
751
+ "adoptsBundledLists": true,
747
752
  "requires": [
748
753
  "tenant"
749
754
  ],
@@ -895,6 +900,7 @@
895
900
  "name": "feature-toggles",
896
901
  "description": "Persists per-feature enabled/disabled state in the `store_global_feature_state` table and exposes a `set` write-handler plus `list`/`registered` query-handlers so operators can flip features at runtime without redeploying. Each API instance keeps an in-memory `GlobalFeatureToggleRuntime` snapshot (initialize it via `createFeatureToggleRuntime`, pass a `() => runtime` accessor to `createFeatureTogglesFeature`) that the dispatcher gate reads on every request; a `toggle-cache-sync` multi-stream projection with `delivery: \"shared\"` syncs the snapshot across instances whenever a `toggle-set` event is appended.",
897
902
  "toggleableDefault": null,
903
+ "adoptsBundledLists": true,
898
904
  "requires": [],
899
905
  "optionalRequires": [],
900
906
  "configReads": [],
@@ -1372,6 +1378,7 @@
1372
1378
  "name": "inbound-mail-foundation",
1373
1379
  "description": "Plugin host for inbound e-mail (inbox federation) — provider plugins (inbound-provider-imap, later M365 Graph / Gmail REST) register at the `inboundMailProvider` extension point with verify/fetch and optional oauth/watch (live push). The foundation owns three event-sourced streams (mail-account lifecycle, inbound-message with exactly-once ingest via deterministic aggregate ids, mail-thread rollup) and their projections `read_mail_accounts`/`read_inbound_messages`/`read_mail_threads`; PII fields are envelope-encrypted per tenant subject key before every append (crypto-shredding on tenant destroy). Wire `createInboundMailConnectRoutes` (OAuth connect + anonymous callback outside /api) and `createInboundMailSupervisor` (IDLE watch with backoff + reconciliation poll) in bin/server.ts. Consume `inbound-mail-foundation:event:inbound-message-received` from app features to attach business processes.",
1374
1380
  "toggleableDefault": null,
1381
+ "adoptsBundledLists": true,
1375
1382
  "requires": [
1376
1383
  "tenant-lifecycle"
1377
1384
  ],
@@ -1464,6 +1471,7 @@
1464
1471
  "name": "jobs",
1465
1472
  "description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops `queued` rows older than `retentionDays`.",
1466
1473
  "toggleableDefault": null,
1474
+ "adoptsBundledLists": true,
1467
1475
  "requires": [],
1468
1476
  "optionalRequires": [],
1469
1477
  "configReads": [],
@@ -1759,6 +1767,7 @@
1759
1767
  "name": "managed-pages",
1760
1768
  "description": "Tenant-editable, server-rendered public pages with per-tenant branding. Stores one Markdown `page` per `(tenantId, slug, lang)` in the `read_pages` entity table with a `published` gate plus `description`/`ogImage` SEO meta. Registers an anonymous `GET {basePath}/:slug` route that resolves the tenant from the request Host via the app-supplied `resolveApexTenant`, serves only published pages (drafts → 404), renders Markdown through the hardened `page-render` core, and isolates per-tenant content with `Vary: Host`. Ships TenantAdmin/SystemAdmin admin screens (`entityList` + `entityEdit`) backed by convention CRUD handlers (`managed-pages:write:page:{create,update,delete}`, `managed-pages:query:page:{list,detail}`); the app wires nav/workspace onto `managed-pages:screen:page-list`. Also exposes `managed-pages:query:by-tenant-published` (anonymous, SQL-filtered on `published`) for sitemap/discovery consumers such as the `seo` feature. Branding (via `config`, scope tenant): `branding-{title,description,site-url,accent-color,logo-url,layout-preset,custom-css}` keys with write-time validation (hex color, https URLs), a `configEdit` self-service screen (`managed-pages:screen:branding-settings`), and a `managed-pages:query:branding` read that the render path applies as scoped `:root` CSS vars + a logo/title header. Also exposes `managed-pages:write:set` (idempotent slug-keyed upsert, SystemAdmin cross-tenant via `tenantIdOverride`) as a provisioning API. Requires `config` + `anonymousAccess` wired at app bootstrap.",
1761
1769
  "toggleableDefault": null,
1770
+ "adoptsBundledLists": true,
1762
1771
  "requires": [
1763
1772
  "config"
1764
1773
  ],
@@ -2088,6 +2097,7 @@
2088
2097
  "name": "personal-access-tokens",
2089
2098
  "description": "Long-lived, revocable Personal Access Tokens for headless HTTP-API access. Stores SHA-256 token hashes in the `store_api_tokens` direct-write table; the plaintext is returned once at creation. `create`/`revoke`/`mine` manage a user's own tokens and `available-scopes` lists the app-declared scope catalog. Bearer tokens carrying the PAT prefix are resolved before jwt.verify (roles resolved live, granted scopes enforced fail-closed at the API boundary) — registered as an auth-foundation tokenVerifier provider, resolved generically by the middleware. Pass { toggleable: { default: false } } to tier-gate the whole feature.",
2090
2099
  "toggleableDefault": null,
2100
+ "adoptsBundledLists": true,
2091
2101
  "requires": [
2092
2102
  "user",
2093
2103
  "tenant",
@@ -2417,6 +2427,7 @@
2417
2427
  "name": "sessions",
2418
2428
  "description": "Tracks signed-in clients in the `store_user_sessions` table (one row per JWT, keyed by the `sid`/`jti` claim) and exposes handlers for `mine` (list your sessions), `revoke`, and `revokeAllOthers`. Session creation and revocation on the hot auth path are handled by `createSessionCallbacks()`, wired into `buildServer({ auth: { ... } })` outside the dispatcher; the same callbacks are also registered as an auth-foundation sessionStore provider, resolvable generically via `resolveSessionStore()`. The feature also ships a manual-trigger cleanup job for pruning expired rows and an optional `autoRevokeOnPasswordChange` hook that mass-revokes all sessions for a user whenever their `passwordHash` changes.",
2419
2429
  "toggleableDefault": null,
2430
+ "adoptsBundledLists": true,
2420
2431
  "requires": [
2421
2432
  "user",
2422
2433
  "auth-foundation"
@@ -2467,6 +2478,123 @@
2467
2478
  "recommended": false
2468
2479
  }
2469
2480
  },
2481
+ {
2482
+ "name": "subscription-dodo",
2483
+ "description": "Dodo Payments provider plugin for `billing-foundation`. Dodo acts as merchant of record: it sells to the customer and issues invoice and VAT, so the consumer-protection texts for this provider come from `consumerProtection.providerTexts`. Reads its API key and webhook secret from system config keys with `backing:\"secrets\"` (envelope-encrypted in the secrets store under the system tenant), plus a `billingLive` master switch and an `environment` key (test | live). `environment` is its own key because Dodo serves test and live mode from different API hosts and a live key must never hit the test host or the other way round; `billingLive` only decides whether checkouts are offered at all. All four are runtime config, so keys rotate and prod goes live without a redeploy, and the `mask` on each derives the sysadmin settings screen and nav. Mount via `createSubscriptionDodoFeature({ priceToTier })`; the optional `apiKey`/`webhookSecret` options are env to secrets bridge fallbacks (`DODO_PAYMENTS_API_KEY`, `DODO_PAYMENTS_WEBHOOK_KEY`). The plugin always mounts; `createCheckoutSession` throws `feature_disabled` unless `billingLive` is true. Webhooks arrive at `/api/subscription/webhook/dodo` and are verified per the Standard Webhooks spec (HMAC-SHA256 over id, timestamp and the raw body, 5 minute tolerance); events for products outside `priceToTier` or without tenant metadata are dropped. Implements hosted checkout, customer portal, cancel and resume, `retrievePrices` (10-minute TTL cache), `retrieveSubscription` for the sync job, and an in-app plan switch: `previewPlanSwitch` shows the proration before the customer confirms, `applyPlanSwitch` changes the plan without a hosted page and waits for the `plan_changed` webhook to move the tier. A plan switch in Dodo requires all plan products to belong to one Product Collection.",
2484
+ "toggleableDefault": null,
2485
+ "requires": [
2486
+ "billing-foundation",
2487
+ "config",
2488
+ "secrets"
2489
+ ],
2490
+ "optionalRequires": [],
2491
+ "configReads": [],
2492
+ "exposesApis": [],
2493
+ "usesApis": [],
2494
+ "extensionsUsed": [
2495
+ {
2496
+ "extensionName": "subscriptionProvider",
2497
+ "entityName": "dodo"
2498
+ }
2499
+ ],
2500
+ "configKeys": [
2501
+ {
2502
+ "key": "api-key",
2503
+ "qualifiedName": "subscription-dodo:config:api-key",
2504
+ "type": "text",
2505
+ "scope": "system",
2506
+ "default": null,
2507
+ "encrypted": true,
2508
+ "computed": false,
2509
+ "validated": false,
2510
+ "options": null,
2511
+ "bounds": null,
2512
+ "pattern": null,
2513
+ "writeRoles": [
2514
+ "SystemAdmin"
2515
+ ],
2516
+ "readRoles": [
2517
+ "TenantAdmin",
2518
+ "Admin",
2519
+ "SystemAdmin"
2520
+ ]
2521
+ },
2522
+ {
2523
+ "key": "billing-live",
2524
+ "qualifiedName": "subscription-dodo:config:billing-live",
2525
+ "type": "boolean",
2526
+ "scope": "system",
2527
+ "default": false,
2528
+ "encrypted": false,
2529
+ "computed": false,
2530
+ "validated": false,
2531
+ "options": null,
2532
+ "bounds": null,
2533
+ "pattern": null,
2534
+ "writeRoles": [
2535
+ "system",
2536
+ "SystemAdmin"
2537
+ ],
2538
+ "readRoles": [
2539
+ "TenantAdmin",
2540
+ "Admin",
2541
+ "SystemAdmin"
2542
+ ]
2543
+ },
2544
+ {
2545
+ "key": "environment",
2546
+ "qualifiedName": "subscription-dodo:config:environment",
2547
+ "type": "select",
2548
+ "scope": "system",
2549
+ "default": "test",
2550
+ "encrypted": false,
2551
+ "computed": false,
2552
+ "validated": false,
2553
+ "options": [
2554
+ "test",
2555
+ "live"
2556
+ ],
2557
+ "bounds": null,
2558
+ "pattern": null,
2559
+ "writeRoles": [
2560
+ "SystemAdmin"
2561
+ ],
2562
+ "readRoles": [
2563
+ "TenantAdmin",
2564
+ "Admin",
2565
+ "SystemAdmin"
2566
+ ]
2567
+ },
2568
+ {
2569
+ "key": "webhook-secret",
2570
+ "qualifiedName": "subscription-dodo:config:webhook-secret",
2571
+ "type": "text",
2572
+ "scope": "system",
2573
+ "default": null,
2574
+ "encrypted": true,
2575
+ "computed": false,
2576
+ "validated": false,
2577
+ "options": null,
2578
+ "bounds": null,
2579
+ "pattern": null,
2580
+ "writeRoles": [
2581
+ "SystemAdmin"
2582
+ ],
2583
+ "readRoles": [
2584
+ "TenantAdmin",
2585
+ "Admin",
2586
+ "SystemAdmin"
2587
+ ]
2588
+ }
2589
+ ],
2590
+ "secrets": [],
2591
+ "writeHandlers": [],
2592
+ "uiHints": {
2593
+ "displayLabel": "Billing · Dodo Payments",
2594
+ "category": "billing",
2595
+ "recommended": false
2596
+ }
2597
+ },
2470
2598
  {
2471
2599
  "name": "subscription-mollie",
2472
2600
  "description": "Mollie payment provider plugin for `billing-foundation`, covering the DACH/EU mid-market use case. Mount via `createSubscriptionMollieFeature({ apiKey, webhookUrl, priceToTier, priceToConfig })` — the factory validates that `priceToTier` and `priceToConfig` keys are identical at boot time. Implements `verifyAndParseWebhook` (lazy Mollie-API fetch + heuristic event-type mapping) and `createCheckoutSession` (customer + first-payment with `sequenceType=\"first\"`); `createPortalSession` and `cancelSubscription` are not available because Mollie has no customer portal and requires a `customerId` that the plugin contract does not carry.",
@@ -2594,6 +2722,7 @@
2594
2722
  "name": "tags",
2595
2723
  "description": "Generic, host-agnostic tagging for any entity. Owns two event-sourced entities — the per-tenant `tag` catalog (`read_tags`, with optional `color` and `scope`) and `tag-assignment` join rows keyed by (entityType, entityId) (`read_tag_assignments`) — so tagging adds NO column to the host entity and needs no relational pivot or JOIN. Catalog screens are declarative (`entityList` + `entityEdit`) and use convention QNs `tag:{create,update,delete}`; TagManager/TagPicker keep `create-tag`/`update-tag`/`delete-tag`. Also: `assign-tag` (idempotent), `remove-tag` (idempotent) and list queries for the catalog and the assignments. Read which tags an entity has, or which entities carry a tag, by listing `tag-assignment` filtered on `entityId` or `tagId` and composing in the read-layer. A tag with empty `scope` is global; a `scope` of an entityType restricts it to that type in the picker. Every path uses one access rule — adopt the host's model with createTagsFeature({ access: { openToAll: { reason } } }) or pin roles with createTagsFeature({ roles }). Pass { toggleable: { default: false } } to make the whole feature tier-gatable via the tier-engine (no host hook).",
2596
2724
  "toggleableDefault": null,
2725
+ "adoptsBundledLists": true,
2597
2726
  "requires": [],
2598
2727
  "optionalRequires": [],
2599
2728
  "configReads": [],
@@ -2709,6 +2838,7 @@
2709
2838
  "name": "tenant",
2710
2839
  "description": "Registers the three core multi-tenancy entities — `tenant`, `tenant-membership`, and `tenant-invitation` (DB tables `read_tenants`, `read_tenant_memberships`, and `read_tenant_invitations`) — along with write handlers for create/update/disable/enable/addMember/removeMember/updateMemberRoles and the matching queries. It also declares a set of per-tenant config keys (companyName, timezone, locale, SMTP credentials) and system-only keys (priceModel, maxUsers) via `r.config({ keys: { ... } })`. Use this feature in every multi-tenant app; membership resolution and invitation flows depend on it, and `auth-email-password` requires it.",
2711
2840
  "toggleableDefault": null,
2841
+ "adoptsBundledLists": true,
2712
2842
  "requires": [
2713
2843
  "config"
2714
2844
  ],
@@ -3075,6 +3205,7 @@
3075
3205
  "name": "user",
3076
3206
  "description": "Manages the cross-tenant user identity: the `read_users` table holds each user's email, `displayName`, global `roles`, `emailVerified` flag, and lifecycle `status` (active / restricted / deletionRequested / deleted). Because users exist above any individual tenant, the feature runs with `r.systemScope()` — membership and tenant-specific roles live in the `tenant` feature instead. Add this feature whenever your app needs a persistent, tenant-agnostic user record that auth and GDPR pipelines can reference.",
3077
3207
  "toggleableDefault": null,
3208
+ "adoptsBundledLists": true,
3078
3209
  "requires": [],
3079
3210
  "optionalRequires": [],
3080
3211
  "configReads": [],
@@ -3102,6 +3233,7 @@
3102
3233
  "name": "user-data-rights",
3103
3234
  "description": "Implements GDPR Art. 15 (access / `my-audit-log` query), Art. 17 (erasure / `request-deletion` + `cancel-deletion`, plus the anonymous email-verified `request-deletion-by-email` + `confirm-deletion-by-token` flow for lockout-safe self-service, + cron cleanup with grace period), Art. 18 (restriction / `restrict-account` + `lift-restriction`), and Art. 20 (portability / async `request-export` → ZIP via `file-foundation`, Magic-Link download) as first-class HTTP handlers and cron jobs. Each domain feature opts in by calling `r.useExtension(EXT_USER_DATA, \"<entity>\", { export, delete })` — the feature then orchestrates the export and forget pipelines across all registered hooks automatically. When `mail-foundation` and a `mail-transport-*` are mounted, it also sends the four GDPR notifications (export ready/failed, deletion requested/executed) itself with no app callback code, rendered in each recipient’s locale. Requires `user`, `data-retention`, `compliance-profiles`, and `sessions`.",
3104
3235
  "toggleableDefault": null,
3236
+ "adoptsBundledLists": true,
3105
3237
  "requires": [
3106
3238
  "user",
3107
3239
  "data-retention",
@@ -3269,6 +3401,7 @@
3269
3401
  "name": "waitlist",
3270
3402
  "description": "Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response body is identical whether or not an address was already listed), and mails the submitter a confirmation. SystemAdmins review entries in a list screen and invite or reject them; an invite creates the entrant's own tenant (or joins one shared tenant) and mails an accept link with the configured membership role, never a global role. Entries are platform-wide, their personal data is crypto-shredding-ready, and the EXT_USER_DATA hooks export and erase entries matching a user's email.",
3271
3403
  "toggleableDefault": null,
3404
+ "adoptsBundledLists": true,
3272
3405
  "requires": [
3273
3406
  "tenant",
3274
3407
  "auth-email-password",
@@ -11,6 +11,7 @@
11
11
  import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
12
12
  import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
13
13
  import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
14
+ import { BUNDLED_LISTS } from "../src/bundled-lists";
14
15
  import { APP_FEATURES, AUTH_COMPOSE_OPTIONS, VALIDATE_BOOT_OPTIONS } from "../src/run-config";
15
16
 
16
17
  export { VALIDATE_BOOT_OPTIONS };
@@ -18,6 +19,7 @@ export { VALIDATE_BOOT_OPTIONS };
18
19
  export const FEATURES: readonly FeatureDefinition[] = composeFeatures(APP_FEATURES, {
19
20
  includeBundled: true,
20
21
  authOptions: AUTH_COMPOSE_OPTIONS,
22
+ bundledLists: BUNDLED_LISTS,
21
23
  });
22
24
 
23
25
  // collectTableMetas erfasst neben entities auch
@@ -12,11 +12,13 @@ import {
12
12
  } from "@cosmicdrift/kumiko-server-runtime/compose-features";
13
13
  import { notesFeature } from "./src/app/notes-feature";
14
14
  import { appScreensFeature } from "./src/app/screens-feature";
15
+ import { BUNDLED_LISTS } from "./src/bundled-lists";
15
16
  import { APP_FEATURES, AUTH_COMPOSE_OPTIONS } from "./src/run-config";
16
17
 
17
18
  export default {
18
19
  features: composeFeatures([...APP_FEATURES, appScreensFeature, notesFeature], {
19
20
  includeBundled: true,
21
+ bundledLists: BUNDLED_LISTS,
20
22
  ...(buildComposeAuthOptions(AUTH_COMPOSE_OPTIONS) !== undefined && {
21
23
  authOptions: buildComposeAuthOptions(AUTH_COMPOSE_OPTIONS),
22
24
  }),
@@ -33,6 +33,7 @@
33
33
  "@cosmicdrift/kumiko-locale-it": "workspace:*"
34
34
  },
35
35
  "devDependencies": {
36
- "@cosmicdrift/kumiko-testing": "workspace:*"
36
+ "@cosmicdrift/kumiko-testing": "workspace:*",
37
+ "fflate": "^0.8.2"
37
38
  }
38
39
  }
@@ -13,6 +13,7 @@
13
13
  import { createTemplateResolverApi } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
14
14
  import type { RunDevAppOptions } from "@cosmicdrift/kumiko-dev-server";
15
15
  import { SYSTEM_TENANT_ID, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
16
+ import { BUNDLED_LISTS } from "../bundled-lists";
16
17
  import { AUTH_COMPOSE_OPTIONS, BOOT_FEATURES, VALIDATE_BOOT_OPTIONS } from "../run-config";
17
18
  import { ADMIN_EMAIL, ADMIN_PASSWORD, BETA_TENANT_ID, DEV_TENANT_ID } from "./auth-constants";
18
19
  import { invoiceReportsFeature } from "./invoice-reports-feature";
@@ -31,6 +32,7 @@ export const devAppOptions = {
31
32
  features: [...BOOT_FEATURES, appScreensFeature, notesFeature, invoiceReportsFeature],
32
33
  featureToggles: true,
33
34
  validateBootOptions: VALIDATE_BOOT_OPTIONS,
35
+ bundledLists: BUNDLED_LISTS,
34
36
  port: Number.parseInt(process.env["PORT"] ?? "4186", 10),
35
37
  clientEntry: "./src/app/client.tsx",
36
38
  htmlPath: "./public/index.html",
@@ -45,6 +45,7 @@ export const noteEntity = createEntity({
45
45
  required: true,
46
46
  maxLength: 200,
47
47
  sortable: true,
48
+ searchable: true,
48
49
  }),
49
50
  customFields: customFieldsField(),
50
51
  },
@@ -65,6 +66,7 @@ const noteListScreen: EntityListScreenDefinition = {
65
66
  },
66
67
  ],
67
68
  searchable: true,
69
+ table: { export: { xlsx: true } },
68
70
  defaultSort: { field: "title", dir: "asc" },
69
71
  slots: { header: { react: { __component: TAGS_FILTER_EXTENSION_NAME } } },
70
72
  rowActions: [
@@ -0,0 +1,15 @@
1
+ import type { BundledListsOptions } from "@cosmicdrift/kumiko-framework/engine";
2
+
3
+ export const BUNDLED_LISTS = {
4
+ table: {
5
+ rememberState: true,
6
+ resetAll: true,
7
+ columnPicker: true,
8
+ density: { userChoice: true },
9
+ pinning: true,
10
+ resizing: true,
11
+ keyboardNavigation: true,
12
+ export: { xlsx: true },
13
+ },
14
+ pinFirstColumn: true,
15
+ } as const satisfies BundledListsOptions;
@@ -75,6 +75,7 @@ import { createRendererPdfFeature } from "@cosmicdrift/kumiko-bundled-features/r
75
75
  import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
76
76
  import { createSeoFeature } from "@cosmicdrift/kumiko-bundled-features/seo";
77
77
  import { createStepDispatcherFeature } from "@cosmicdrift/kumiko-bundled-features/step-dispatcher";
78
+ import { createSubscriptionDodoFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-dodo";
78
79
  import { createSubscriptionMollieFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-mollie";
79
80
  import { createSubscriptionStripeFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
80
81
  import { createTagsFeature } from "@cosmicdrift/kumiko-bundled-features/tags";
@@ -209,6 +210,11 @@ export const APP_FEATURES = [
209
210
  webhookSecret: "whsec_smoke",
210
211
  priceToTier: { price_smoke: "free" },
211
212
  }),
213
+ createSubscriptionDodoFeature({
214
+ apiKey: "dodo_smoke",
215
+ webhookSecret: "whsec_smoke",
216
+ priceToTier: { pdt_smoke: "free" },
217
+ }),
212
218
  createSubscriptionMollieFeature({
213
219
  apiKey: "test_smoke_key",
214
220
  webhookUrl: "https://smoke.example/webhook",
@@ -416,12 +422,12 @@ export const BOOT_FEATURES = APP_FEATURES.filter(
416
422
  (feature) => feature.name !== FEATURE_TOGGLES_FEATURE,
417
423
  );
418
424
 
419
- // Smoke signup — enables createAuthSelfRegistrationToggleFeature via
420
- // composeFeatures when passed as authOptions (#1521 Option A). Same shape
421
- // as run{Dev,Prod}App's auth.signup; appUrl is a stub (no real mail in boot).
422
425
  // locale-de is mounted, so every translation key must exist in German and English.
423
426
  export const VALIDATE_BOOT_OPTIONS = { requiredLocales: ["de", "en"] } as const;
424
427
 
428
+ // Smoke signup — enables createAuthSelfRegistrationToggleFeature via
429
+ // composeFeatures when passed as authOptions (#1521 Option A). Same shape
430
+ // as run{Dev,Prod}App's auth.signup; appUrl is a stub (no real mail in boot).
425
431
  export const AUTH_COMPOSE_OPTIONS = {
426
432
  signup: {
427
433
  appUrl: "http://localhost:4186/signup",
@@ -75,8 +75,13 @@ features: [..., createSessionsFeature(), mfaFeature, createAccountSecurityFeatur
75
75
  5. **MFA state changes revoke every OTHER live session** (stolen-session defense) — never the session that made the change.
76
76
  6. **Enforcement is opt-in.** Without a tenant enabling `auth-mfa:config:required`, users choose for themselves whether to enroll.
77
77
 
78
+ ## Confirmation code input
79
+
80
+ `src/web/confirmation-code-step.tsx` shows the `Input kind="code"` renderer primitive outside the MFA screen, for example an e-mail or invite confirmation code. Pass `length` (here 6), `autoFocus` and `onComplete`. The input accepts digits only, so pasting "123 456" yields `123456`, and `onComplete` fires once when the value reaches `length`. `autoComplete` defaults to `one-time-code`, so mobile keyboards offer the code from the SMS or mail. After a wrong code the step clears the value and bumps a `key`, which remounts the input and returns focus to it.
81
+
78
82
  ## Tests
79
83
 
80
84
  - **Baseline:** a user without MFA enrolled still gets a JWT straight from `/auth/login`.
81
85
  - **End-to-end:** enroll via `enable-start`/`enable-confirm`, log in (gets a challenge, no JWT), complete via `/auth/mfa/verify` (gets a JWT) — copies the `buildServer({ auth: ... })` block from the sample comment verbatim.
82
86
  - **Account security:** the composed screen boot-validates next to `auth-mfa` and `sessions` and shows a sidebar entry once `nav` is set.
87
+ - **Confirmation code step:** typing or pasting six digits calls `onConfirm` once; a rejected code shows the error, clears the field and refocuses it.
@@ -4,9 +4,12 @@
4
4
  "private": true,
5
5
  "dependencies": {
6
6
  "@cosmicdrift/kumiko-bundled-features": "workspace:*",
7
- "@cosmicdrift/kumiko-framework": "workspace:*"
7
+ "@cosmicdrift/kumiko-framework": "workspace:*",
8
+ "@cosmicdrift/kumiko-renderer": "workspace:*",
9
+ "react": "^19.2.6"
8
10
  },
9
11
  "devDependencies": {
10
- "@cosmicdrift/kumiko-testing": "workspace:*"
12
+ "@cosmicdrift/kumiko-testing": "workspace:*",
13
+ "@types/react": "^19.2.14"
11
14
  }
12
15
  }
@@ -0,0 +1,49 @@
1
+ import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
2
+ import { type ReactNode, useState } from "react";
3
+
4
+ const CONFIRMATION_CODE_LENGTH = 6;
5
+
6
+ export type ConfirmationCodeStepProps = {
7
+ /** Resolves true when the server accepted the code. */
8
+ readonly onConfirm: (code: string) => Promise<boolean>;
9
+ };
10
+
11
+ export function ConfirmationCodeStep({ onConfirm }: ConfirmationCodeStepProps): ReactNode {
12
+ const { Input, Banner } = usePrimitives();
13
+ const [code, setCode] = useState("");
14
+ const [attempt, setAttempt] = useState(0);
15
+ const [pending, setPending] = useState(false);
16
+ const [rejected, setRejected] = useState(false);
17
+
18
+ const confirm = async (submittedCode: string): Promise<void> => {
19
+ if (pending) return;
20
+ setPending(true);
21
+ setRejected(false);
22
+ const accepted = await onConfirm(submittedCode).finally(() => setPending(false));
23
+ if (accepted) return;
24
+ setRejected(true);
25
+ setCode("");
26
+ // The input was disabled while pending and lost focus; remounting restores autoFocus.
27
+ setAttempt((n) => n + 1);
28
+ };
29
+
30
+ return (
31
+ <>
32
+ <Input
33
+ key={attempt}
34
+ kind="code"
35
+ length={CONFIRMATION_CODE_LENGTH}
36
+ id="confirmation-code"
37
+ name="confirmation-code"
38
+ testId="confirmation-code"
39
+ value={code}
40
+ onChange={setCode}
41
+ onComplete={(next) => void confirm(next)}
42
+ disabled={pending}
43
+ hasError={rejected}
44
+ autoFocus
45
+ />
46
+ {rejected ? <Banner variant="error">That code is not valid. Try again.</Banner> : null}
47
+ </>
48
+ );
49
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
5
+ "jsx": "react-jsx"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -10,7 +10,8 @@ instead of keeping a hand-written copy of every feature's variables.
10
10
  `kmsEnvSchema` and `s3EnvSchema`. `composeEnvSchema` collects them and
11
11
  records which feature declared each variable.
12
12
  - **App-level blocks in `extend`**: `buildInfoEnvSchema`, `demoAdminEnvSchema`,
13
- `stripeEnvSchema({ prices })` and `createS3EnvSchema({ prefix })` for a
13
+ `stripeEnvSchema({ prices })`, `dodoEnvSchema({ prices })` and
14
+ `createS3EnvSchema({ prefix })` for a
14
15
  second bucket. These belong to no mounted feature, so the app adds them.
15
16
  - **Typed result**: `parseEnv(composed.schema, …)` returns the merged type,
16
17
  so `parsed.S3_BUCKET` is `string | undefined` and `parsed.APP_PUBLIC_URL` is
@@ -26,6 +27,7 @@ instead of keeping a hand-written copy of every feature's variables.
26
27
  | `smtpEnvSchema` | `channel-email` | `createChannelEmailFeature()` |
27
28
  | `s3EnvSchema`, `createS3EnvSchema({ prefix })` | `file-provider-s3` | `createFileProviderS3Feature({ fromEnv: true })` |
28
29
  | `stripeEnvSchema({ prices })` | `subscription-stripe` | app (`extend`); the feature registers the bridge keys |
30
+ | `dodoEnvSchema({ prices })` | `subscription-dodo` | app (`extend`); the feature registers the bridge keys |
29
31
  | `mfaTokenSecretsEnvSchema` | `auth-mfa` | `createAuthMfaFeature()` |
30
32
  | `buildInfoEnvSchema`, `demoAdminEnvSchema` | `foundation-shared` | app (`extend`) |
31
33
  | `prometheusMetricsEnvSchema` | `framework/observability` | app (`extend`) |
@@ -15,6 +15,7 @@ import {
15
15
  demoAdminEnvSchema,
16
16
  } from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
17
17
  import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
18
+ import { dodoEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-dodo";
18
19
  import { stripeEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
19
20
  import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";
20
21
  import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
@@ -36,6 +37,7 @@ const appEnvSchema = z.object({
36
37
  ...buildInfoEnvSchema.shape,
37
38
  ...demoAdminEnvSchema.shape,
38
39
  ...stripeEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
40
+ ...dodoEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
39
41
  ...createS3EnvSchema({ prefix: "RELEASES_S3_" }).shape,
40
42
  APP_PUBLIC_URL: z.url().describe("Public origin of the app, used in mail links."),
41
43
  });
@@ -7,7 +7,7 @@ import {
7
7
  emitEvent,
8
8
  setFields,
9
9
  } from "@cosmicdrift/kumiko-framework/engine";
10
- import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
10
+ import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
11
11
  import { z } from "zod";
12
12
 
13
13
  export const ticketEntity = createEntity({
@@ -43,6 +43,7 @@ export const helpdeskFeature = defineFeature("helpdesk", (r) => {
43
43
  if (!ticket) {
44
44
  return failNotFound("ticket", event.payload.id);
45
45
  }
46
+ if (ticket["isClosed"]) return failUnprocessable("ticket_already_closed");
46
47
  await emitEvent(ctx, ticketClosed, {
47
48
  aggregateId: event.payload.id,
48
49
  aggregateType: "ticket",
@@ -72,7 +72,7 @@ await runProdApp({
72
72
  // Required (1): routes run anonymous, need tenant resolution
73
73
  anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
74
74
  // Required (2): boot check + internal lookup use ctx.templateResolver
75
- extraContext: ({ db }) => ({
75
+ extraContext: ({ db, registry }) => ({
76
76
  templateResolver: createTemplateResolverApi(db, registry),
77
77
  }),
78
78
  });