@cosmicdrift/kumiko-samples 0.393.0 → 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 +143 -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 -2
  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 +93 -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 +238 -89
  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 +30 -0
  114. package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +124 -41
  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 +2 -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 -49
  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 +9 -0
  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 +4 -6
  127. package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +75 -45
  128. package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +19 -8
  129. package/packages/bundled-features/src/inbound-provider-imap/screens.ts +203 -109
  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 +87 -38
  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 +8 -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; `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.",
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",
@@ -1454,6 +1460,7 @@
1454
1460
  "secrets": [],
1455
1461
  "writeHandlers": [
1456
1462
  "inbound-provider-imap:write:connect-mailbox",
1463
+ "inbound-provider-imap:write:connect-my-mailbox",
1457
1464
  "inbound-provider-imap:write:reconnect-mailbox"
1458
1465
  ],
1459
1466
  "uiHints": {
@@ -2505,8 +2512,34 @@
2505
2512
  }
2506
2513
  },
2507
2514
  {
2508
- "name": "subscription-dodo",
2509
- "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.",
2510
2543
  "toggleableDefault": null,
2511
2544
  "requires": [
2512
2545
  "billing-foundation",
@@ -2520,13 +2553,13 @@
2520
2553
  "extensionsUsed": [
2521
2554
  {
2522
2555
  "extensionName": "subscriptionProvider",
2523
- "entityName": "dodo"
2556
+ "entityName": "paddle"
2524
2557
  }
2525
2558
  ],
2526
2559
  "configKeys": [
2527
2560
  {
2528
2561
  "key": "api-key",
2529
- "qualifiedName": "subscription-dodo:config:api-key",
2562
+ "qualifiedName": "subscription-paddle:config:api-key",
2530
2563
  "type": "text",
2531
2564
  "scope": "system",
2532
2565
  "default": null,
@@ -2547,7 +2580,7 @@
2547
2580
  },
2548
2581
  {
2549
2582
  "key": "billing-live",
2550
- "qualifiedName": "subscription-dodo:config:billing-live",
2583
+ "qualifiedName": "subscription-paddle:config:billing-live",
2551
2584
  "type": "boolean",
2552
2585
  "scope": "system",
2553
2586
  "default": false,
@@ -2567,17 +2600,38 @@
2567
2600
  "SystemAdmin"
2568
2601
  ]
2569
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
+ },
2570
2624
  {
2571
2625
  "key": "environment",
2572
- "qualifiedName": "subscription-dodo:config:environment",
2626
+ "qualifiedName": "subscription-paddle:config:environment",
2573
2627
  "type": "select",
2574
2628
  "scope": "system",
2575
- "default": "test",
2629
+ "default": "sandbox",
2576
2630
  "encrypted": false,
2577
2631
  "computed": false,
2578
2632
  "validated": false,
2579
2633
  "options": [
2580
- "test",
2634
+ "sandbox",
2581
2635
  "live"
2582
2636
  ],
2583
2637
  "bounds": null,
@@ -2592,8 +2646,8 @@
2592
2646
  ]
2593
2647
  },
2594
2648
  {
2595
- "key": "webhook-secret",
2596
- "qualifiedName": "subscription-dodo:config:webhook-secret",
2649
+ "key": "tenant-binding-key",
2650
+ "qualifiedName": "subscription-paddle:config:tenant-binding-key",
2597
2651
  "type": "text",
2598
2652
  "scope": "system",
2599
2653
  "default": null,
@@ -2611,38 +2665,33 @@
2611
2665
  "Admin",
2612
2666
  "SystemAdmin"
2613
2667
  ]
2614
- }
2615
- ],
2616
- "secrets": [],
2617
- "writeHandlers": [],
2618
- "uiHints": {
2619
- "displayLabel": "Billing · Dodo Payments",
2620
- "category": "billing",
2621
- "recommended": false
2622
- }
2623
- },
2624
- {
2625
- "name": "subscription-mollie",
2626
- "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.",
2627
- "toggleableDefault": null,
2628
- "requires": [
2629
- "billing-foundation"
2630
- ],
2631
- "optionalRequires": [],
2632
- "configReads": [],
2633
- "exposesApis": [],
2634
- "usesApis": [],
2635
- "extensionsUsed": [
2668
+ },
2636
2669
  {
2637
- "extensionName": "subscriptionProvider",
2638
- "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
+ ]
2639
2689
  }
2640
2690
  ],
2641
- "configKeys": [],
2642
2691
  "secrets": [],
2643
2692
  "writeHandlers": [],
2644
2693
  "uiHints": {
2645
- "displayLabel": "Billing · Mollie",
2694
+ "displayLabel": "Billing · Paddle",
2646
2695
  "category": "billing",
2647
2696
  "recommended": false
2648
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
  });