@cosmicdrift/kumiko-samples 0.393.0 → 0.395.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 (226) 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 +36 -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 +61 -136
  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-imap/watch-mailbox.ts +109 -0
  133. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
  134. package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
  135. package/packages/bundled-features/src/jobs/changes.json +18 -0
  136. package/packages/bundled-features/src/managed-pages/changes.json +7 -0
  137. package/packages/bundled-features/src/managed-pages/constants.ts +7 -0
  138. package/packages/bundled-features/src/managed-pages/feature.ts +6 -4
  139. package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +2 -0
  140. package/packages/bundled-features/src/managed-pages/handlers/page-body-guard.ts +61 -0
  141. package/packages/bundled-features/src/managed-pages/handlers/page-body-issues.ts +26 -0
  142. package/packages/bundled-features/src/managed-pages/handlers/page-crud.ts +17 -10
  143. package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +15 -4
  144. package/packages/bundled-features/src/managed-pages/render-page-body.ts +28 -0
  145. package/packages/bundled-features/src/managed-pages/screens/page-screens.ts +6 -2
  146. package/packages/bundled-features/src/managed-pages/seeding.ts +13 -2
  147. package/packages/bundled-features/src/managed-pages/table.ts +6 -2
  148. package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +6 -4
  149. package/packages/bundled-features/src/managed-pages/web/constants.ts +14 -0
  150. package/packages/bundled-features/src/managed-pages/web/i18n.ts +14 -0
  151. package/packages/bundled-features/src/managed-pages/web/lossy-markdown.ts +71 -0
  152. package/packages/bundled-features/src/managed-pages/web/markdown-to-document.ts +27 -0
  153. package/packages/bundled-features/src/managed-pages/web/page-body-section.tsx +73 -0
  154. package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
  155. package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
  156. package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
  157. package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
  158. package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
  159. package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
  160. package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
  161. package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
  162. package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
  163. package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
  164. package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
  165. package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
  166. package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
  167. package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
  168. package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
  169. package/packages/bundled-features/src/template-resolver/README.md +2 -2
  170. package/packages/bundled-features/src/template-resolver/changes.json +20 -0
  171. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +16 -16
  172. package/packages/bundled-features/src/template-resolver/feature.ts +14 -1
  173. package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
  174. package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
  175. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +24 -6
  176. package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +32 -0
  177. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -0
  178. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +9 -3
  179. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +13 -480
  180. package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +105 -0
  181. package/packages/bundled-features/src/template-resolver/web/collection-lookup.ts +33 -0
  182. package/packages/bundled-features/src/template-resolver/web/constants.ts +3 -0
  183. package/packages/bundled-features/src/template-resolver/web/content-field.tsx +61 -0
  184. package/packages/bundled-features/src/template-resolver/web/content-tree-provider.ts +256 -0
  185. package/packages/bundled-features/src/template-resolver/web/document-reference-name.ts +12 -0
  186. package/packages/bundled-features/src/template-resolver/web/document-schema.ts +3 -11
  187. package/packages/bundled-features/src/template-resolver/web/i18n.ts +9 -2
  188. package/packages/bundled-features/src/template-resolver/web/legacy-variable-text.ts +30 -0
  189. package/packages/bundled-features/src/template-resolver/web/tenant-override.ts +7 -0
  190. package/packages/bundled-features/src/template-resolver/web/text-block-editor.tsx +92 -0
  191. package/packages/bundled-features/src/template-resolver/web/use-collection-editor-context.ts +42 -0
  192. package/packages/bundled-features/src/template-resolver/web/use-collection-entry-create.ts +92 -0
  193. package/packages/bundled-features/src/template-resolver/web/use-content-write.ts +81 -0
  194. package/packages/bundled-features/src/template-resolver/web/use-text-block-editor.ts +127 -0
  195. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  196. package/packages/bundled-features/src/tier-engine/feature.ts +9 -1
  197. package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
  198. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
  199. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
  200. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
  201. package/samples/apps/use-all-bundled/feature-manifest.json +89 -40
  202. package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
  203. package/samples/apps/use-all-bundled/package.json +1 -0
  204. package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
  205. package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
  206. package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
  207. package/samples/apps/use-all-bundled/src/app/client.tsx +8 -1
  208. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
  209. package/samples/apps/use-all-bundled/src/app/seed.ts +17 -2
  210. package/samples/apps/use-all-bundled/src/run-config.ts +19 -11
  211. package/samples/recipes/cross-feature-events/README.md +10 -0
  212. package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
  213. package/samples/recipes/dashboard-stats/README.md +8 -0
  214. package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
  215. package/samples/recipes/env-composition/README.md +2 -2
  216. package/samples/recipes/env-composition/src/env.ts +2 -2
  217. package/samples/recipes/inbound-mail-triage/README.md +8 -0
  218. package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
  219. package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
  220. package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
  221. package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
  222. package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
  223. package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
  224. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
  225. package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
  226. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
@@ -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,7 +26,9 @@ 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";
31
+ import { managedPagesClient } from "@cosmicdrift/kumiko-bundled-features/managed-pages/web";
30
32
  import { sessionsClient } from "@cosmicdrift/kumiko-bundled-features/sessions/web";
31
33
  import { tagsClient } from "@cosmicdrift/kumiko-bundled-features/tags/web";
32
34
  import { textBlocksClient } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
@@ -99,6 +101,9 @@ createKumikoApp({
99
101
  shell: AppShell,
100
102
  clientFeatures: [
101
103
  localeDeClient(),
104
+ userDataRightsClient({
105
+ publicDeletion: { requestPath: "/account/delete", confirmPath: "/account/delete/confirm" },
106
+ }),
102
107
  emailPasswordClient({ mfaVerifyScreen: MfaVerifyScreen }),
103
108
  authMfaClient(),
104
109
  adminShellClient(),
@@ -112,14 +117,16 @@ createKumikoApp({
112
117
  tierEngineClient(),
113
118
  capOverviewClient(),
114
119
  userProfileClient(),
115
- userDataRightsClient(),
116
120
  customFieldsClient(),
117
121
  foldersClient(),
122
+ inboundMailFoundationClient(),
118
123
  complianceProfilesClient(),
119
124
  featureTogglesClient(),
120
125
  // tag-list management screen (TagManager) + TagSection/TagFilter extension
121
126
  // slots + tag i18n. Required for the dev-only notesFeature host to render.
122
127
  tagsClient(),
128
+ // Document editor of the page-edit body section.
129
+ managedPagesClient(),
123
130
  // template-resolver text-block tree + editor under the app-owned "content"
124
131
  // nav node. Points at SYSTEM_TENANT because that is where the legal and
125
132
  // marketing copy is seeded.
@@ -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
  },
@@ -335,7 +335,19 @@ export const seedScreenshotData: SeedFn = async (stack) => {
335
335
  slug: "about",
336
336
  lang: "en",
337
337
  title: "About Acme",
338
- body: "# About Acme\n\nWe build calm software.",
338
+ body: [
339
+ "# About Acme",
340
+ "",
341
+ "We build calm software for small teams. Acme was founded in 2019 in Hamburg and now supports more than 400 companies across Europe.",
342
+ "",
343
+ "## What we stand for",
344
+ "",
345
+ "- Software that stays out of your way",
346
+ "- Fair, per-seat pricing without surprises",
347
+ "- Your data stays in the EU",
348
+ "",
349
+ "Read more about how we handle data in our [privacy policy](/legal/privacy).",
350
+ ].join("\n"),
339
351
  published: true,
340
352
  });
341
353
  await seedPage(stack.db, {
@@ -468,10 +480,13 @@ export const seedScreenshotData: SeedFn = async (stack) => {
468
480
  // carry whatever the handler derives instead of a hand-built projection.
469
481
  // SYSTEM_TENANT, not the dev tenant: the screenshot admin reads the system
470
482
  // tenant — the same reason the text-block tree above seeds there.
483
+ // The real admin, not TestUsers.systemAdmin's id: the seed signs in with a live session row,
484
+ // and the session list shows that row's user. The sessionChecker re-derives roles from the
485
+ // admin's global SystemAdmin role, which the collection's access accepts.
471
486
  const snippetAuthor: SessionUser = {
472
487
  ...TestUsers.systemAdmin,
473
488
  tenantId: SYSTEM_TENANT_ID,
474
- roles: ["TenantAdmin"],
489
+ id: devAdminId,
475
490
  };
476
491
  for (const snippet of [
477
492
  {
@@ -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",
@@ -314,10 +316,10 @@ export const APP_FEATURES = [
314
316
  label: "templateResolver:nav.snippets",
315
317
  workspaces: ["admin-shell:workspace:platform"],
316
318
  },
317
- // rich + variableSchema so the smoke app renders the WYSIWYG with
319
+ // document + variableSchema so the smoke app renders the WYSIWYG with
318
320
  // insertable chips, not just the plain textarea — the preview
319
321
  // substitutes these example values for `{{customerName}}` & co.
320
- contentFormat: "rich",
322
+ contentFormat: "document",
321
323
  variableSchema: {
322
324
  customerName: "Max Mustermann",
323
325
  orderId: "A-1042",
@@ -333,7 +335,7 @@ export const APP_FEATURES = [
333
335
  label: "templateResolver:nav.signatures",
334
336
  workspaces: ["admin-shell:workspace:platform"],
335
337
  },
336
- contentFormat: "rich",
338
+ contentFormat: "document",
337
339
  },
338
340
  ],
339
341
  }),
@@ -350,12 +352,18 @@ export const APP_FEATURES = [
350
352
  // PUPPETEER_SERVICE_URL + PUPPETEER_AUTH_TOKEN and only asked when a PDF is generated.
351
353
  createRendererPuppeteerClientFeature(),
352
354
 
353
- // managed-pages: requires config (auto-bundled). Smoke resolver never serves
354
- // (boot-only). allowCustomCss:true boot-validates the CSS code path + emits
355
+ // managed-pages: requires config (auto-bundled). Every host resolves to the
356
+ // dev tenant so the document-editor e2e can read a saved page back from the
357
+ // public route. allowCustomCss:true boot-validates the CSS code path + emits
355
358
  // the branding-custom-css key into the manifest. The managed-pages-css
356
359
  // companion toggle is handler-less, has no subpath export, and is recipe-
357
360
  // covered — not standalone-mounted here (smoke = one mount per subpath-export).
358
- createManagedPagesFeature({ resolveApexTenant: () => null, allowCustomCss: true }),
361
+ createManagedPagesFeature({
362
+ // Mapping every host to the dev tenant is for this sample and its e2e only; production maps
363
+ // its known hosts and returns null for any other.
364
+ resolveApexTenant: () => DEV_TENANT_ID as TenantId,
365
+ allowCustomCss: true,
366
+ }),
359
367
 
360
368
  // Template overview, layout editor and PDF template editor; needs template-resolver and managed-pages above.
361
369
  createDocumentDesignerFeature(),
@@ -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
  });
@@ -28,6 +28,14 @@ mail-triage ← this sample (the app-side cons
28
28
 
29
29
  When the provider rejects the stored login, the supervisor sets the account to `auth_error` and stops fetching. With `inbound-provider-imap` mounted, the mail accounts list offers **Reconnect** on that row: the form `mail-account-reconnect-imap` is prefilled with the stored server settings (never the password), `inbound-provider-imap:write:reconnect-mailbox` checks the new login and puts the same account back to `active`. Threads, sync cursor and seen messages stay, so the next poll continues where the account stopped instead of ingesting the mailbox again. The cases are covered in `inbound-provider-imap/__tests__/reconnect-mailbox.integration.test.ts`; they need greenmail, which this sample's in-memory provider does not.
30
30
 
31
+ ## Mail content, quarantine and status
32
+
33
+ Every ingested mail now carries its full text (`bodyText`, at most 32,000 characters, flagged by `bodyTextTruncated`), the original (`.eml`, or `.html` for HTML-only mails) and its attachments as file references; `inbound-mail-foundation:query:message:content` returns them, while `message:list` stays light and never carries the body. Retention removes the mail row but keeps the attachment files. Needs file storage in the app; without it the mail is still imported and the mailbox shows **Unterbrochen** (`file_storage_unavailable`).
34
+
35
+ A mail the provider cannot parse is set aside in quarantine instead of blocking the mailbox: the rest imports, the sync cursor moves on, the account shows `degraded` with `lastError: message_quarantined` and the count, and `acknowledge-quarantine` clears it. Three failed polls in a row (`connection_failed`, `rate_limited`) also mark the mailbox `degraded` until the next success.
36
+
37
+ A disconnected mailbox keeps its account id: `reactivate-account` (or connecting the same OAuth address again) puts it back to `active` with its cursor, so nothing is imported twice, and the mail arrives right away instead of at the next poll. Connecting an address that is already connected is refused with 409.
38
+
31
39
  ## Tests
32
40
 
33
41
  - E2E through `createAllInOneEntrypoint` (HTTP → dispatcher → BullMQ worker lane, no framework-internal shortcuts): connect → ingest → triage item appears.
@@ -4,7 +4,9 @@
4
4
  // FileRef: renderTablePdf draws the table (pure JS, no browser). The storage
5
5
  // write runs in the beforeTransaction phase, so the transaction holds no pool
6
6
  // connection while the bytes upload; the handler then only creates the
7
- // fileRef row from event.prepared.
7
+ // fileRef row from event.prepared. Called from another handler (a nested
8
+ // write) there is no such phase, so the handler renders and stores inside the
9
+ // caller's transaction instead.
8
10
 
9
11
  import { renderTablePdf } from "@cosmicdrift/kumiko-bundled-features/renderer-pdf";
10
12
  import {
@@ -19,6 +21,7 @@ import {
19
21
  } from "@cosmicdrift/kumiko-framework/engine";
20
22
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
21
23
  import {
24
+ createFileRefFromBytes,
22
25
  createFileRefFromUploadedBytes,
23
26
  uploadFileBytesBeforeTransaction,
24
27
  } from "@cosmicdrift/kumiko-framework/files";
@@ -60,6 +63,62 @@ export const pdfAuditExportFeature = defineFeature("pdf-audit-export", (r) => {
60
63
  r.entity({ name: "inspection", ...inspectionEntity });
61
64
  const { executor: inspectionExecutor } = createEntityExecutor("inspection", inspectionEntity);
62
65
 
66
+ type AuditPayload = { assetName: string; from: string; to: string };
67
+ type ExecutorUser = Parameters<typeof inspectionExecutor.list>[1];
68
+ type ExecutorDb = Parameters<typeof inspectionExecutor.list>[2];
69
+
70
+ async function renderAudit(payload: AuditPayload, user: ExecutorUser, db: ExecutorDb) {
71
+ const { assetName, from, to } = payload;
72
+ const inspections = await inspectionExecutor.list(
73
+ {
74
+ filters: [
75
+ { field: "assetName", op: "eq", value: assetName },
76
+ { field: "inspectedOn", op: "gte", value: from },
77
+ { field: "inspectedOn", op: "lte", value: to },
78
+ ],
79
+ sort: "inspectedOn",
80
+ sortDirection: "asc",
81
+ limit: AUDIT_ROW_LIMIT + 1,
82
+ },
83
+ user,
84
+ db,
85
+ );
86
+ // A protocol that silently drops inspections would look complete.
87
+ if (inspections.rows.length > AUDIT_ROW_LIMIT) {
88
+ return writeFailure(
89
+ new UnprocessableError("audit_period_too_large", {
90
+ details: { limit: AUDIT_ROW_LIMIT },
91
+ }),
92
+ );
93
+ }
94
+
95
+ const { pdfBytes } = await renderTablePdf({
96
+ title: `Prüfprotokoll ${assetName}`,
97
+ meta: [
98
+ { label: "Zeitraum", value: `${formatGermanDate(from)} bis ${formatGermanDate(to)}` },
99
+ { label: "Prüfungen", value: String(inspections.rows.length) },
100
+ ],
101
+ columns: [{ header: "Datum" }, { header: "Ergebnis" }, { header: "Prüfer", weight: 2 }],
102
+ rows: inspections.rows.map((row) => [
103
+ formatGermanDate(String(row["inspectedOn"])),
104
+ RESULT_LABELS[z.enum(INSPECTION_RESULTS).parse(row["result"])],
105
+ String(row["inspector"] ?? ""),
106
+ ]),
107
+ emptyText: "Keine Prüfungen im Zeitraum",
108
+ footerText: `Prüfbericht ${assetName}`,
109
+ pageLabel: "Seite {page} von {pages}",
110
+ });
111
+
112
+ return {
113
+ isSuccess: true as const,
114
+ data: {
115
+ fileName: `audit-${toFileNameSegment(assetName)}.pdf`,
116
+ mimeType: "application/pdf",
117
+ data: pdfBytes,
118
+ },
119
+ };
120
+ }
121
+
63
122
  r.writeHandler({
64
123
  name: "inspection:create",
65
124
  schema: z.object({
@@ -77,56 +136,20 @@ export const pdfAuditExportFeature = defineFeature("pdf-audit-export", (r) => {
77
136
  name: "export-audit",
78
137
  schema: z.object({ assetName: z.string().min(1), from: z.iso.date(), to: z.iso.date() }),
79
138
  access: access.access,
139
+ // Nested in another command there is no phase of its own; the handler then does this work itself.
140
+ beforeTransactionWhenNested: "skip",
80
141
  beforeTransaction: async (event, ctx) => {
81
- const { assetName, from, to } = event.payload;
82
- const inspections = await inspectionExecutor.list(
83
- {
84
- filters: [
85
- { field: "assetName", op: "eq", value: assetName },
86
- { field: "inspectedOn", op: "gte", value: from },
87
- { field: "inspectedOn", op: "lte", value: to },
88
- ],
89
- sort: "inspectedOn",
90
- sortDirection: "asc",
91
- limit: AUDIT_ROW_LIMIT + 1,
92
- },
93
- event.user,
94
- ctx.db,
95
- );
96
- // A protocol that silently drops inspections would look complete.
97
- if (inspections.rows.length > AUDIT_ROW_LIMIT) {
98
- return writeFailure(
99
- new UnprocessableError("audit_period_too_large", {
100
- details: { limit: AUDIT_ROW_LIMIT },
101
- }),
102
- );
103
- }
104
-
105
- const { pdfBytes } = await renderTablePdf({
106
- title: `Prüfprotokoll ${assetName}`,
107
- meta: [
108
- { label: "Zeitraum", value: `${formatGermanDate(from)} bis ${formatGermanDate(to)}` },
109
- { label: "Prüfungen", value: String(inspections.rows.length) },
110
- ],
111
- columns: [{ header: "Datum" }, { header: "Ergebnis" }, { header: "Prüfer", weight: 2 }],
112
- rows: inspections.rows.map((row) => [
113
- formatGermanDate(String(row["inspectedOn"])),
114
- RESULT_LABELS[z.enum(INSPECTION_RESULTS).parse(row["result"])],
115
- String(row["inspector"] ?? ""),
116
- ]),
117
- emptyText: "Keine Prüfungen im Zeitraum",
118
- footerText: `Prüfbericht ${assetName}`,
119
- pageLabel: "Seite {page} von {pages}",
120
- });
121
-
122
- return uploadFileBytesBeforeTransaction(ctx, {
123
- fileName: `audit-${toFileNameSegment(assetName)}.pdf`,
124
- mimeType: "application/pdf",
125
- data: pdfBytes,
126
- });
142
+ const rendered = await renderAudit(event.payload, event.user, ctx.db);
143
+ if (!rendered.isSuccess) return rendered;
144
+ return uploadFileBytesBeforeTransaction(ctx, rendered.data);
127
145
  },
128
146
  handler: async (event, ctx) => {
129
- const created = await createFileRefFromUploadedBytes(ctx, event.prepared);
147
+ const created =
148
+ event.prepared !== undefined
149
+ ? await createFileRefFromUploadedBytes(ctx, event.prepared)
150
+ : await renderAudit(event.payload, event.user, ctx.db).then((rendered) =>
151
+ rendered.isSuccess ? createFileRefFromBytes(ctx, rendered.data) : rendered,
152
+ );
130
153
  if (!created.isSuccess) return created;
131
154
  return { isSuccess: true as const, data: { fileRefId: created.data.id } };
132
155
  },