@cosmicdrift/kumiko-samples 0.392.2 → 0.394.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 (186) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +15 -5
  3. package/packages/bundled-features/src/account-security/README.md +1 -1
  4. package/packages/bundled-features/src/account-security/changes.json +6 -0
  5. package/packages/bundled-features/src/account-security/feature.ts +9 -1
  6. package/packages/bundled-features/src/audit/event-classification.ts +1 -0
  7. package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
  8. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
  9. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
  10. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
  11. package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
  12. package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
  13. package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
  14. package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
  15. package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
  16. package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
  17. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
  18. package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
  19. package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
  20. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
  21. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
  22. package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
  23. package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
  24. package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
  25. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
  26. package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
  27. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
  28. package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
  29. package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
  30. package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
  31. package/packages/bundled-features/src/cap-overview/changes.json +6 -0
  32. package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
  33. package/packages/bundled-features/src/cap-overview/types.ts +2 -0
  34. package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
  35. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
  36. package/packages/bundled-features/src/files/changes.json +18 -0
  37. package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
  38. package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
  39. package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
  40. package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
  41. package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
  42. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +155 -0
  43. package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
  44. package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
  45. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
  46. package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
  47. package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
  48. package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
  49. package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
  50. package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
  51. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
  52. package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
  53. package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
  54. package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
  55. package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
  56. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
  57. package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
  58. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
  59. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
  60. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
  61. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
  62. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
  63. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
  64. package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
  65. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
  66. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
  67. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
  68. package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
  69. package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
  70. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
  71. package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
  72. package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
  73. package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
  74. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
  75. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -1
  76. package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
  77. package/packages/bundled-features/src/inbound-mail-foundation/index.ts +94 -15
  78. package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
  79. package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
  80. package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
  81. package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
  82. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
  83. package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
  84. package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
  85. package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
  89. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
  90. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
  91. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
  92. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
  93. package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
  94. package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
  95. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
  96. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +239 -79
  97. package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
  98. package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
  99. package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
  100. package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
  101. package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
  102. package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
  103. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
  104. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
  105. package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
  106. package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
  107. package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
  108. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
  109. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
  110. package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
  111. package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
  112. package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
  113. package/packages/bundled-features/src/inbound-provider-imap/changes.json +42 -0
  114. package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +155 -63
  115. package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
  116. package/packages/bundled-features/src/inbound-provider-imap/constants.ts +14 -1
  117. package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
  118. package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
  119. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +75 -44
  120. package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
  121. package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +28 -2
  122. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
  123. package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
  124. package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
  125. package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
  126. package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +45 -0
  127. package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +103 -0
  128. package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +49 -0
  129. package/packages/bundled-features/src/inbound-provider-imap/screens.ts +209 -42
  130. package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
  131. package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
  132. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
  133. package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
  134. package/packages/bundled-features/src/jobs/changes.json +18 -0
  135. package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -0
  136. package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
  137. package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
  138. package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
  139. package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
  140. package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
  141. package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
  142. package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
  143. package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
  144. package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
  145. package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
  146. package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
  147. package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
  148. package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
  149. package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
  150. package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
  151. package/packages/bundled-features/src/template-resolver/changes.json +12 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
  153. package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +25 -0
  155. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +218 -117
  156. package/packages/bundled-features/src/template-resolver/web/i18n.ts +1 -2
  157. package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
  158. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
  159. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
  160. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
  161. package/samples/apps/use-all-bundled/feature-manifest.json +89 -39
  162. package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
  163. package/samples/apps/use-all-bundled/package.json +1 -0
  164. package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
  165. package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
  166. package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
  167. package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
  168. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
  169. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -1
  170. package/samples/apps/use-all-bundled/src/run-config.ts +7 -5
  171. package/samples/recipes/cross-feature-events/README.md +10 -0
  172. package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
  173. package/samples/recipes/dashboard-stats/README.md +8 -0
  174. package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
  175. package/samples/recipes/env-composition/README.md +2 -2
  176. package/samples/recipes/env-composition/src/env.ts +2 -2
  177. package/samples/recipes/inbound-mail-triage/README.md +12 -0
  178. package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
  179. package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
  180. package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
  181. package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
  182. package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
  183. package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
  184. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
  185. package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
  186. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
@@ -316,7 +316,7 @@
316
316
  },
317
317
  {
318
318
  "name": "billing-foundation",
319
- "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.",
319
+ "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-paddle` unless you are writing a new payment provider.",
320
320
  "toggleableDefault": null,
321
321
  "requires": [
322
322
  "tenant-lifecycle",
@@ -1421,9 +1421,15 @@
1421
1421
  "configKeys": [],
1422
1422
  "secrets": [],
1423
1423
  "writeHandlers": [
1424
+ "inbound-mail-foundation:write:acknowledge-quarantine",
1425
+ "inbound-mail-foundation:write:attach-message-content",
1424
1426
  "inbound-mail-foundation:write:connect-account",
1425
1427
  "inbound-mail-foundation:write:disconnect-account",
1426
1428
  "inbound-mail-foundation:write:ingest-message",
1429
+ "inbound-mail-foundation:write:mark-credential-stored",
1430
+ "inbound-mail-foundation:write:reactivate-account",
1431
+ "inbound-mail-foundation:write:retry-quarantined-message",
1432
+ "inbound-mail-foundation:write:set-trusted-authserv-id",
1427
1433
  "inbound-mail-foundation:write:update-account"
1428
1434
  ],
1429
1435
  "uiHints": {
@@ -1434,7 +1440,7 @@
1434
1440
  },
1435
1441
  {
1436
1442
  "name": "inbound-provider-imap",
1437
- "description": "Registers the `\"imap\"` provider for `inbound-mail-foundation` using imapflow — password/app-password auth covers classic IMAP hosts without any OAuth (plus an XOAUTH2 fallback consuming an access token from the secret slot). Incremental sync via a UIDVALIDITY:lastUid cursor (UIDVALIDITY change triggers a cursor-invalid full resync), initial backfill bounded by the foundation backfill window, and live push via IMAP IDLE for the watch supervisor. Tenant admins connect a mailbox through the \"Connect mailbox\" form on the mail accounts list (`connect-mailbox` checks the IMAP login and the SMTP login of the outgoing server first, then creates the account and its credential secret in one transaction); programmatic setups store the per-account connection JSON (host/port/secure/user/password, plus smtpHost/smtpPort for sending) in the account credential secret slot before the first sync. Implements the provider `send`: `sendMailFromAccount` sends over the SMTP server of the mailbox itself (implicit TLS, same login) with the mailbox address as From and stores a copy in its sent folder.",
1443
+ "description": "Registers the `\"imap\"` provider for `inbound-mail-foundation` using imapflow — password/app-password auth covers classic IMAP hosts without any OAuth (plus an XOAUTH2 fallback consuming an access token from the secret slot). Incremental sync via a UIDVALIDITY:lastUid cursor (UIDVALIDITY change triggers a cursor-invalid full resync), initial backfill bounded by the foundation backfill window, and live push via IMAP IDLE for the watch supervisor. Tenant admins connect a shared mailbox through the \"Connect mailbox\" form on the mail accounts list and members a personal one through the same form on \"My mail accounts\" (`connect-mailbox` and `connect-my-mailbox` check the IMAP login and the SMTP login of the outgoing server first, then creates the account and its credential secret in one transaction; `reconnect-mailbox` does the same check for an existing mailbox with a sign-in error and overwrites its credential secret); programmatic setups store the per-account connection JSON (host/port/secure/user/password, plus smtpHost/smtpPort for sending) in the account credential secret slot before the first sync. Implements the provider `send`: `sendMailFromAccount` sends over the SMTP server of the mailbox itself (implicit TLS, same login) with the mailbox address as From and stores a copy in its sent folder.",
1438
1444
  "toggleableDefault": null,
1439
1445
  "requires": [
1440
1446
  "inbound-mail-foundation",
@@ -1453,7 +1459,9 @@
1453
1459
  "configKeys": [],
1454
1460
  "secrets": [],
1455
1461
  "writeHandlers": [
1456
- "inbound-provider-imap:write:connect-mailbox"
1462
+ "inbound-provider-imap:write:connect-mailbox",
1463
+ "inbound-provider-imap:write:connect-my-mailbox",
1464
+ "inbound-provider-imap:write:reconnect-mailbox"
1457
1465
  ],
1458
1466
  "uiHints": {
1459
1467
  "displayLabel": "Inbound Mail · IMAP",
@@ -2504,8 +2512,34 @@
2504
2512
  }
2505
2513
  },
2506
2514
  {
2507
- "name": "subscription-dodo",
2508
- "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.",
2515
+ "name": "subscription-mollie",
2516
+ "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.",
2517
+ "toggleableDefault": null,
2518
+ "requires": [
2519
+ "billing-foundation"
2520
+ ],
2521
+ "optionalRequires": [],
2522
+ "configReads": [],
2523
+ "exposesApis": [],
2524
+ "usesApis": [],
2525
+ "extensionsUsed": [
2526
+ {
2527
+ "extensionName": "subscriptionProvider",
2528
+ "entityName": "mollie"
2529
+ }
2530
+ ],
2531
+ "configKeys": [],
2532
+ "secrets": [],
2533
+ "writeHandlers": [],
2534
+ "uiHints": {
2535
+ "displayLabel": "Billing · Mollie",
2536
+ "category": "billing",
2537
+ "recommended": false
2538
+ }
2539
+ },
2540
+ {
2541
+ "name": "subscription-paddle",
2542
+ "description": "Paddle Billing provider plugin for `billing-foundation`. Paddle 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, webhook secret and tenant binding key from system config keys with `backing:\"secrets\"` (envelope-encrypted in the secrets store under the system tenant), the public Paddle.js `client-token`, a `billingLive` master switch and an `environment` key (sandbox | live). `environment` is its own key because Paddle serves sandbox and live from different API hosts and a live key must never hit the sandbox host or the other way round; `billingLive` only decides whether checkouts are offered at all. All of them 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 `createSubscriptionPaddleFeature({ priceToTier })`; the optional `apiKey`/`webhookSecret`/`tenantBindingKey`/`clientToken` options are env to config bridge fallbacks (`PADDLE_API_KEY`, `PADDLE_WEBHOOK_SECRET`, `PADDLE_TENANT_BINDING_KEY`, `PADDLE_CLIENT_TOKEN`). The plugin always mounts; `createCheckoutSession` throws `feature_disabled` unless `billingLive` is true, and billing counts as enabled only with API key and binding key present. Webhooks arrive at `/webhooks/subscription/paddle` when mounted through the server-runtime `billing.paddle` boot option, or at `/api/subscription/webhook/paddle` when `createSubscriptionWebhookRoute` is mounted by hand and are verified with the `Paddle-Signature` header (HMAC-SHA256 over `ts:rawBody` keyed with the raw secret, any `h1` may match, 5 minute tolerance). Paddle.js can open checkouts with arbitrary `customData` from the browser, so `custom_data.tenantId` is only trusted together with `tenantBinding`, an HMAC of the tenant id under the binding key; events without a valid binding, for prices outside `priceToTier` or without a tenant are dropped. Rotating the binding key makes the webhooks of existing subscriptions fail that check until they are re-bound. Paddle has no hosted checkout page, so the feature serves the Default Payment Link itself at `/billing/paddle-checkout` (the Default Payment Link in the Paddle dashboard must point to `<APP_BASE_URL>/billing/paddle-checkout`; it is the only source, because checkouts are created without a `checkout.url`). Create subscription prices in the Paddle catalog with quantity minimum and maximum 1; for one-off packs set the quantity limits on purpose, because the payment event carries `quantity` and the app has to multiply its credits by it: an anonymous page with its own CSP that loads Paddle.js from cdn.paddle.com, the one external script this framework allows, and opens the overlay for the transaction. Implements checkout, customer portal, cancel and resume, `retrievePrices` (batched, 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 `subscription.updated` webhook to move the tier.",
2509
2543
  "toggleableDefault": null,
2510
2544
  "requires": [
2511
2545
  "billing-foundation",
@@ -2519,13 +2553,13 @@
2519
2553
  "extensionsUsed": [
2520
2554
  {
2521
2555
  "extensionName": "subscriptionProvider",
2522
- "entityName": "dodo"
2556
+ "entityName": "paddle"
2523
2557
  }
2524
2558
  ],
2525
2559
  "configKeys": [
2526
2560
  {
2527
2561
  "key": "api-key",
2528
- "qualifiedName": "subscription-dodo:config:api-key",
2562
+ "qualifiedName": "subscription-paddle:config:api-key",
2529
2563
  "type": "text",
2530
2564
  "scope": "system",
2531
2565
  "default": null,
@@ -2546,7 +2580,7 @@
2546
2580
  },
2547
2581
  {
2548
2582
  "key": "billing-live",
2549
- "qualifiedName": "subscription-dodo:config:billing-live",
2583
+ "qualifiedName": "subscription-paddle:config:billing-live",
2550
2584
  "type": "boolean",
2551
2585
  "scope": "system",
2552
2586
  "default": false,
@@ -2566,17 +2600,38 @@
2566
2600
  "SystemAdmin"
2567
2601
  ]
2568
2602
  },
2603
+ {
2604
+ "key": "client-token",
2605
+ "qualifiedName": "subscription-paddle:config:client-token",
2606
+ "type": "text",
2607
+ "scope": "system",
2608
+ "default": null,
2609
+ "encrypted": false,
2610
+ "computed": false,
2611
+ "validated": false,
2612
+ "options": null,
2613
+ "bounds": null,
2614
+ "pattern": null,
2615
+ "writeRoles": [
2616
+ "SystemAdmin"
2617
+ ],
2618
+ "readRoles": [
2619
+ "TenantAdmin",
2620
+ "Admin",
2621
+ "SystemAdmin"
2622
+ ]
2623
+ },
2569
2624
  {
2570
2625
  "key": "environment",
2571
- "qualifiedName": "subscription-dodo:config:environment",
2626
+ "qualifiedName": "subscription-paddle:config:environment",
2572
2627
  "type": "select",
2573
2628
  "scope": "system",
2574
- "default": "test",
2629
+ "default": "sandbox",
2575
2630
  "encrypted": false,
2576
2631
  "computed": false,
2577
2632
  "validated": false,
2578
2633
  "options": [
2579
- "test",
2634
+ "sandbox",
2580
2635
  "live"
2581
2636
  ],
2582
2637
  "bounds": null,
@@ -2591,8 +2646,8 @@
2591
2646
  ]
2592
2647
  },
2593
2648
  {
2594
- "key": "webhook-secret",
2595
- "qualifiedName": "subscription-dodo:config:webhook-secret",
2649
+ "key": "tenant-binding-key",
2650
+ "qualifiedName": "subscription-paddle:config:tenant-binding-key",
2596
2651
  "type": "text",
2597
2652
  "scope": "system",
2598
2653
  "default": null,
@@ -2610,38 +2665,33 @@
2610
2665
  "Admin",
2611
2666
  "SystemAdmin"
2612
2667
  ]
2613
- }
2614
- ],
2615
- "secrets": [],
2616
- "writeHandlers": [],
2617
- "uiHints": {
2618
- "displayLabel": "Billing · Dodo Payments",
2619
- "category": "billing",
2620
- "recommended": false
2621
- }
2622
- },
2623
- {
2624
- "name": "subscription-mollie",
2625
- "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.",
2626
- "toggleableDefault": null,
2627
- "requires": [
2628
- "billing-foundation"
2629
- ],
2630
- "optionalRequires": [],
2631
- "configReads": [],
2632
- "exposesApis": [],
2633
- "usesApis": [],
2634
- "extensionsUsed": [
2668
+ },
2635
2669
  {
2636
- "extensionName": "subscriptionProvider",
2637
- "entityName": "mollie"
2670
+ "key": "webhook-secret",
2671
+ "qualifiedName": "subscription-paddle:config:webhook-secret",
2672
+ "type": "text",
2673
+ "scope": "system",
2674
+ "default": null,
2675
+ "encrypted": true,
2676
+ "computed": false,
2677
+ "validated": false,
2678
+ "options": null,
2679
+ "bounds": null,
2680
+ "pattern": null,
2681
+ "writeRoles": [
2682
+ "SystemAdmin"
2683
+ ],
2684
+ "readRoles": [
2685
+ "TenantAdmin",
2686
+ "Admin",
2687
+ "SystemAdmin"
2688
+ ]
2638
2689
  }
2639
2690
  ],
2640
- "configKeys": [],
2641
2691
  "secrets": [],
2642
2692
  "writeHandlers": [],
2643
2693
  "uiHints": {
2644
- "displayLabel": "Billing · Mollie",
2694
+ "displayLabel": "Billing · Paddle",
2645
2695
  "category": "billing",
2646
2696
  "recommended": false
2647
2697
  }
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": 1,
3
+ "tables": ["read_inbound_messages"]
4
+ }
@@ -10,6 +10,7 @@
10
10
  "dev": "PORT=4186 bun --env-file=../../../.env run src/app/server.ts",
11
11
  "screenshots": "bun x playwright test e2e/screenshots.spec.ts --config=playwright.config.ts",
12
12
  "billing-gallery": "bun x playwright test --config=playwright.billing-gallery.config.ts",
13
+ "docs-guides": "bun x playwright test e2e/docs-guides.screenshots.spec.ts --config=playwright.config.ts && bun x playwright test --config=playwright.billing-docs.config.ts",
13
14
  "e2e": "bun x playwright test e2e/mfa-login.spec.ts --config=playwright.config.ts",
14
15
  "gen:manifest": "bun run scripts/gen-feature-manifest.ts",
15
16
  "gen:sample-index": "bun run scripts/gen-sample-index.ts"
@@ -0,0 +1,15 @@
1
+ // @runtime test
2
+ // English docs screenshots of the billing plans screen. Boots the gallery server with the docs
3
+ // variant switched on (net prices with a note, gross recurring price in the plan switch); the
4
+ // German review gallery has its own config. Gated by SCREENSHOT_DIR, see the `billing-docs` script.
5
+ import { defineAppE2eConfig, PLAYWRIGHT_DEMO_ENV } from "@cosmicdrift/kumiko-testing/e2e";
6
+ import { E2E_PORTS } from "../../e2e/e2e-ports";
7
+ import { GALLERY_DOCS_VARIANT_ENV } from "./e2e-billing-gallery/gallery-constants";
8
+
9
+ export default defineAppE2eConfig({
10
+ port: E2E_PORTS["framework/use-all-bundled-billing-gallery"],
11
+ env: { ...PLAYWRIGHT_DEMO_ENV, [GALLERY_DOCS_VARIANT_ENV]: "1" },
12
+ locale: "en",
13
+ testDir: "./e2e-billing-docs",
14
+ serverEntry: "e2e-billing-gallery/server.ts",
15
+ });
@@ -1,6 +1,7 @@
1
1
  import type { CapSpec } from "@cosmicdrift/kumiko-bundled-features/cap-overview";
2
2
  import { noteEntryTable } from "@cosmicdrift/kumiko-bundled-features/notes-history";
3
3
  import { tagEntity } from "@cosmicdrift/kumiko-bundled-features/tags";
4
+ import { templateResourcesTable } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
4
5
  import { tenantMembershipsTable } from "@cosmicdrift/kumiko-bundled-features/tenant";
5
6
 
6
7
  import { buildEntityTable, selectMany } from "@cosmicdrift/kumiko-framework/db";
@@ -11,10 +12,11 @@ const tagTable = buildEntityTable("tag", tagEntity);
11
12
  const NOTE_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 10, pro: 100 };
12
13
  const TAG_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 5, pro: 50 };
13
14
  const SEAT_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 5, pro: 20 };
15
+ const TEMPLATE_LIMIT_BY_TIER: Readonly<Record<string, number>> = { free: 3, pro: 30 };
14
16
  const DEFAULT_LIMIT = 10;
15
17
 
16
- // Four example caps built from entities this sample already mounts
17
- // (notes-history, tags, tenant-membership) — demonstrates the caller-supplied
18
+ // Five example caps built from entities this sample already mounts
19
+ // (notes-history, tags, tenant-membership, template-resolver) — demonstrates the caller-supplied
18
20
  // CapSpec contract cap-overview reads against, the same shape a real app
19
21
  // would provide for its own usage tables.
20
22
  export const CAP_OVERVIEW_CAPS: readonly CapSpec[] = [
@@ -68,4 +70,16 @@ export const CAP_OVERVIEW_CAPS: readonly CapSpec[] = [
68
70
  },
69
71
  icon: "gauge",
70
72
  },
73
+ {
74
+ id: "templates",
75
+ label: "cap-overview.caps.templates",
76
+ limit: (tier) => TEMPLATE_LIMIT_BY_TIER[tier] ?? DEFAULT_LIMIT,
77
+ usage: async (db, tenantId) => {
78
+ const rows = await selectMany(db, templateResourcesTable, { tenantId: [tenantId] });
79
+ return rows.length;
80
+ },
81
+ // Counter only: the limit stays enforced server-side but is not shown to the tenant.
82
+ showLimit: false,
83
+ icon: "file",
84
+ },
71
85
  ];
@@ -1,6 +1,6 @@
1
1
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
2
2
 
3
- // Labels for the four example caps (notes, tags, seats, apiCalls) wired up in
3
+ // Labels for the five example caps (notes, tags, seats, apiCalls, templates) wired up in
4
4
  // cap-overview-caps.ts. Cap ids are the app's choice, so their labels are
5
5
  // too — cap-overview itself carries no vocabulary for app-owned caps, same
6
6
  // reasoning as collection-labels-feature.ts for content collections.
@@ -12,6 +12,7 @@ export const capOverviewLabelsFeature = defineFeature("cap-overview-labels", (r)
12
12
  "cap-overview.caps.tags": { de: "Tags", en: "Tags" },
13
13
  "cap-overview.caps.seats": { de: "Plätze", en: "Seats" },
14
14
  "cap-overview.caps.apiCalls": { de: "API-Aufrufe", en: "API calls" },
15
+ "cap-overview.caps.templates": { de: "Vorlagen", en: "Templates" },
15
16
  },
16
17
  });
17
18
  return {};
@@ -26,6 +26,7 @@ import { customFieldsClient } from "@cosmicdrift/kumiko-bundled-features/custom-
26
26
  import { deliveryClient } from "@cosmicdrift/kumiko-bundled-features/delivery/web";
27
27
  import { featureTogglesClient } from "@cosmicdrift/kumiko-bundled-features/feature-toggles/web";
28
28
  import { foldersClient } from "@cosmicdrift/kumiko-bundled-features/folders/web";
29
+ import { inboundMailFoundationClient } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation/web";
29
30
  import { jobsClient } from "@cosmicdrift/kumiko-bundled-features/jobs/web";
30
31
  import { sessionsClient } from "@cosmicdrift/kumiko-bundled-features/sessions/web";
31
32
  import { tagsClient } from "@cosmicdrift/kumiko-bundled-features/tags/web";
@@ -115,6 +116,7 @@ createKumikoApp({
115
116
  userDataRightsClient(),
116
117
  customFieldsClient(),
117
118
  foldersClient(),
119
+ inboundMailFoundationClient(),
118
120
  complianceProfilesClient(),
119
121
  featureTogglesClient(),
120
122
  // tag-list management screen (TagManager) + TagSection/TagFilter extension
@@ -5,11 +5,13 @@
5
5
  // (#1258): an open screen not reachable via nav is no longer an eligible
6
6
  // fallback candidate.
7
7
 
8
+ import { MAIL_ACCOUNT_LIST_SCREEN_QN } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
8
9
  import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
10
+ import { ADMIN_NAV_QN, ADMIN_NAV_WORKSPACES } from "./admin-nav-feature";
9
11
 
10
12
  export const appScreensFeature: FeatureDefinition = defineFeature("app-screens", (r) => {
11
- r.describe("App-side nav placement for the user-profile self-service page.");
12
- r.requires({ features: ["user-profile"] });
13
+ r.describe("App-side nav placement for the user-profile self-service page and the mailbox list.");
14
+ r.requires({ features: ["user-profile", "inbound-mail-foundation"] });
13
15
  r.nav({
14
16
  id: "profile",
15
17
  label: "screen:profile.title",
@@ -29,8 +31,18 @@ export const appScreensFeature: FeatureDefinition = defineFeature("app-screens",
29
31
  access: { roles: ["SystemAdmin"] },
30
32
  workspaces: ["admin-shell:workspace:platform"],
31
33
  });
34
+ r.nav({
35
+ id: "mail-accounts",
36
+ label: "nav:mail-accounts.title",
37
+ icon: "mail",
38
+ screen: MAIL_ACCOUNT_LIST_SCREEN_QN,
39
+ parent: ADMIN_NAV_QN,
40
+ workspaces: ADMIN_NAV_WORKSPACES,
41
+ order: 20,
42
+ });
32
43
  r.translations({
33
44
  keys: {
45
+ "nav:mail-accounts.title": { de: "Postfächer", en: "Mailboxes" },
34
46
  "screen:profile.title": { de: "Profil", en: "Profile" },
35
47
  "nav:content.title": { de: "Inhalte", en: "Content" },
36
48
  },
@@ -468,10 +468,13 @@ export const seedScreenshotData: SeedFn = async (stack) => {
468
468
  // carry whatever the handler derives instead of a hand-built projection.
469
469
  // SYSTEM_TENANT, not the dev tenant: the screenshot admin reads the system
470
470
  // tenant — the same reason the text-block tree above seeds there.
471
+ // The real admin, not TestUsers.systemAdmin's id: the seed signs in with a live session row,
472
+ // and the session list shows that row's user. The sessionChecker re-derives roles from the
473
+ // admin's global SystemAdmin role, which the collection's access accepts.
471
474
  const snippetAuthor: SessionUser = {
472
475
  ...TestUsers.systemAdmin,
473
476
  tenantId: SYSTEM_TENANT_ID,
474
- roles: ["TenantAdmin"],
477
+ id: devAdminId,
475
478
  };
476
479
  for (const snippet of [
477
480
  {
@@ -75,8 +75,8 @@ 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";
79
78
  import { createSubscriptionMollieFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-mollie";
79
+ import { createSubscriptionPaddleFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-paddle";
80
80
  import { createSubscriptionStripeFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
81
81
  import { createTagsFeature } from "@cosmicdrift/kumiko-bundled-features/tags";
82
82
  import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
@@ -212,10 +212,12 @@ export const APP_FEATURES = [
212
212
  webhookSecret: "whsec_smoke",
213
213
  priceToTier: { price_smoke: "free" },
214
214
  }),
215
- createSubscriptionDodoFeature({
216
- apiKey: "dodo_smoke",
217
- webhookSecret: "whsec_smoke",
218
- priceToTier: { pdt_smoke: "free" },
215
+ createSubscriptionPaddleFeature({
216
+ apiKey: "paddle_smoke",
217
+ webhookSecret: "pdl_ntfset_smoke",
218
+ tenantBindingKey: "paddle_smoke_binding_key",
219
+ clientToken: "test_smoke_client_token",
220
+ priceToTier: { pri_smoke: "free" },
219
221
  }),
220
222
  createSubscriptionMollieFeature({
221
223
  apiKey: "test_smoke_key",
@@ -26,6 +26,13 @@ Three building blocks:
26
26
  external system). With `table`, the apply materializes a persistent
27
27
  read model.
28
28
 
29
+ - **`r.job(name, { trigger: { on: event.name, catchUp: true } }, run)`**
30
+ runs a job per stored event. With `catchUp` the job owns its own
31
+ consumer cursor (`job:<jobQn>`) that starts at the beginning of the
32
+ event store, so a job added to a running app also processes the
33
+ events from before its deploy. Each event enqueues at most one run.
34
+ Without `catchUp` a job only sees events appended after it went live.
35
+
29
36
  ## Flow
30
37
 
31
38
  ```
@@ -61,3 +68,6 @@ The integration test pins three guarantees:
61
68
  ```bash
62
69
  bun kumiko test integration samples/cross-feature-events
63
70
  ```
71
+ 4. **Catch-up job** — `catch-up-job.integration.test.ts` places orders
72
+ before the first dispatcher pass and checks that the `catchUp` job
73
+ invoices each of them exactly once.
@@ -60,6 +60,7 @@ export type CapturedEvent = {
60
60
  readonly tenantId: string;
61
61
  };
62
62
  export const capturedEvents: CapturedEvent[] = [];
63
+ export const invoicedOrderIds: string[] = [];
63
64
 
64
65
  // --- Feature ---
65
66
 
@@ -103,6 +104,19 @@ export const pubsubOrderFeature = defineFeature("pubsubOrders", (r) => {
103
104
  access: { roles: ["Admin", "Customer"] },
104
105
  });
105
106
 
107
+ // A job can react to the same event. `catchUp: true` gives the job its own
108
+ // consumer cursor that starts at the beginning of the event store, so a job
109
+ // added to a running app also processes the orders placed before it existed.
110
+ // Without catchUp the job only sees events appended after its deploy.
111
+ r.job(
112
+ "invoice-order",
113
+ { trigger: { on: orderPlaced.name, catchUp: true }, runIn: "worker" },
114
+ async (payload) => {
115
+ const order = orderPlaced.schema.parse(payload);
116
+ invoicedOrderIds.push(order.id);
117
+ },
118
+ );
119
+
106
120
  // Async consumer — Marten-style. Fires after commit, via the event-
107
121
  // dispatcher. No `table` means this MSP is pure side-effect (no state
108
122
  // persisted). Delivery is at-least-once and ordered per consumer.
@@ -20,6 +20,14 @@ query and the fields to read, the query does the counting.
20
20
  `list` with `filter`, `limit: 1` and `totalCount: true`. The total is counted
21
21
  over the whole WHERE clause, so no rows are loaded just to be counted.
22
22
  - **Labels are translation keys**: declared with `r.translations`.
23
+ - **Record-bound dashboard**: `ticket-record-dashboard` binds to a record with `record`
24
+ and embeds the `ticket-card` projectionDetail as a screen panel; the record id
25
+ becomes its entityId.
26
+ - **Gated toolbar button**: `visibleWhen` on a toolbar action asks a flat
27
+ capability query; the button is hidden while it loads or fails.
28
+ - **Fallback by reason**: `fallback: { forbidden, notFound }` renders a screen on
29
+ 403/404 of the gate query; other reasons get the default notice or an error
30
+ banner with retry.
23
31
 
24
32
  ## When to reach for it
25
33
 
@@ -1,6 +1,8 @@
1
1
  // Dashboard Stats Sample
2
2
  // Shows: a dashboard screen with a stat-group and a single stat, backed by one
3
- // query handler that counts in the database.
3
+ // query handler that counts in the database. A second, record-bound dashboard embeds a
4
+ // projectionDetail as a panel, hides a toolbar button behind a capability query, and picks its
5
+ // fallback screen by the reason the gate closed.
4
6
 
5
7
  import {
6
8
  createEntity,
@@ -22,6 +24,7 @@ export const ticketEntity = createEntity({
22
24
  });
23
25
 
24
26
  export const TICKET_STATS_QUERY = "tickets:query:ticket:stats";
27
+ export const TICKET_CAPABILITIES_QUERY = "tickets:query:ticket:capabilities";
25
28
 
26
29
  const readerRoles = { roles: ["Admin", "User"] } as const;
27
30
 
@@ -57,8 +60,51 @@ export const ticketFeature = defineFeature("tickets", (r) => {
57
60
  access: readerRoles,
58
61
  });
59
62
 
63
+ // Gate for the screens below: a flat answer with one boolean the screens test with `eq`.
64
+ r.queryHandler({
65
+ name: "ticket:capabilities",
66
+ schema: z.object({}),
67
+ outputSchema: z.object({ canClose: z.boolean() }),
68
+ handler: async (query) => ({ canClose: query.user.roles.includes("Admin") }),
69
+ access: readerRoles,
70
+ });
71
+
72
+ r.queryHandler({
73
+ name: "ticket:card",
74
+ schema: z.object({ id: z.string() }),
75
+ handler: async (query, ctx) => {
76
+ const page = await ticketExecutor.list(
77
+ { filter: { field: "id", op: "eq", value: query.payload.id }, limit: 1 },
78
+ query.user,
79
+ ctx.db,
80
+ );
81
+ const ticket = page.rows[0];
82
+ return { id: query.payload.id, title: ticket?.title ?? "", status: ticket?.status ?? "" };
83
+ },
84
+ access: readerRoles,
85
+ });
86
+
87
+ r.queryHandler({
88
+ name: "ticket:rows",
89
+ schema: z.object({}),
90
+ handler: async (query, ctx) => {
91
+ const page = await ticketExecutor.list({ limit: 50 }, query.user, ctx.db);
92
+ return { rows: page.rows, nextCursor: null };
93
+ },
94
+ access: readerRoles,
95
+ });
96
+
60
97
  r.translations({
61
98
  keys: {
99
+ "screen:ticket-list.title": { en: "Tickets" },
100
+ "screen:ticket-card.title": { en: "Ticket" },
101
+ "screen:ticket-record-dashboard.title": { en: "Ticket overview" },
102
+ "entity:ticket:field:title": { en: "Title" },
103
+ "entity:ticket:field:status": { en: "Status" },
104
+ "entity:__projection-detail__:field:title": { en: "Title" },
105
+ "entity:__projection-detail__:field:status": { en: "Status" },
106
+ "tickets.toolbar.close": { en: "Close all done" },
107
+ "tickets.dashboard.card": { en: "Ticket" },
62
108
  "tickets.dashboard.group": { en: "Tickets by status" },
63
109
  "tickets.dashboard.open": { en: "Open" },
64
110
  "tickets.dashboard.closed": { en: "Closed" },
@@ -101,4 +147,48 @@ export const ticketFeature = defineFeature("tickets", (r) => {
101
147
  ],
102
148
  access: readerRoles,
103
149
  });
150
+
151
+ // Three additions: a projectionDetail embedded in a record-bound dashboard, a gated toolbar
152
+ // button, and a fallback chosen by reason.
153
+ const gate = { query: TICKET_CAPABILITIES_QUERY, field: "canClose", eq: true } as const;
154
+
155
+ // The list doubles as the fallback target: it has no visibleWhen of its own.
156
+ r.screen({
157
+ id: "ticket-list",
158
+ type: "projectionList",
159
+ query: "tickets:query:ticket:rows",
160
+ columns: ["title", "status"],
161
+ access: readerRoles,
162
+ toolbarActions: [
163
+ {
164
+ kind: "writeHandler",
165
+ id: "close-done",
166
+ label: "tickets.toolbar.close",
167
+ handler: "tickets:write:ticket:update",
168
+ // Live and fail-closed: hidden while the query loads and when it fails.
169
+ visibleWhen: gate,
170
+ },
171
+ ],
172
+ });
173
+
174
+ r.screen({
175
+ id: "ticket-card",
176
+ type: "projectionDetail",
177
+ query: "tickets:query:ticket:card",
178
+ layout: { sections: [{ title: "tickets.dashboard.card", fields: ["title", "status"] }] },
179
+ access: readerRoles,
180
+ visibleWhen: gate,
181
+ // 403 and 404 on the gate query get their own screen; an unmet condition keeps the
182
+ // default notice and any other failure shows an error banner with retry.
183
+ fallback: { forbidden: "ticket-list", notFound: "ticket-list" },
184
+ });
185
+
186
+ // `record` reads the id from the URL; the embedded projectionDetail receives it as its entityId.
187
+ r.screen({
188
+ id: "ticket-record-dashboard",
189
+ type: "dashboard",
190
+ record: { param: "ticketId", payloadKey: "id" },
191
+ panels: [{ kind: "screen", id: "card", screen: "ticket-card" }],
192
+ access: readerRoles,
193
+ });
104
194
  });
@@ -10,7 +10,7 @@ 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 })`, `dodoEnvSchema({ prices })` and
13
+ `stripeEnvSchema({ prices })`, `paddleEnvSchema({ prices })` and
14
14
  `createS3EnvSchema({ prefix })` for a
15
15
  second bucket. These belong to no mounted feature, so the app adds them.
16
16
  - **Typed result**: `parseEnv(composed.schema, …)` returns the merged type,
@@ -27,7 +27,7 @@ instead of keeping a hand-written copy of every feature's variables.
27
27
  | `smtpEnvSchema` | `channel-email` | `createChannelEmailFeature()` |
28
28
  | `s3EnvSchema`, `createS3EnvSchema({ prefix })` | `file-provider-s3` | `createFileProviderS3Feature({ fromEnv: true })` |
29
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 |
30
+ | `paddleEnvSchema({ prices })` | `subscription-paddle` | app (`extend`); the feature registers the bridge keys |
31
31
  | `mfaTokenSecretsEnvSchema` | `auth-mfa` | `createAuthMfaFeature()` |
32
32
  | `buildInfoEnvSchema`, `demoAdminEnvSchema` | `foundation-shared` | app (`extend`) |
33
33
  | `prometheusMetricsEnvSchema` | `framework/observability` | app (`extend`) |
@@ -15,7 +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
+ import { paddleEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-paddle";
19
19
  import { stripeEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
20
20
  import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";
21
21
  import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
@@ -37,7 +37,7 @@ const appEnvSchema = z.object({
37
37
  ...buildInfoEnvSchema.shape,
38
38
  ...demoAdminEnvSchema.shape,
39
39
  ...stripeEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
40
- ...dodoEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
40
+ ...paddleEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
41
41
  ...createS3EnvSchema({ prefix: "RELEASES_S3_" }).shape,
42
42
  APP_PUBLIC_URL: z.url().describe("Public origin of the app, used in mail links."),
43
43
  });