@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
@@ -13,7 +13,9 @@
13
13
  import type { ConfigAccessor, Registry } from "@cosmicdrift/kumiko-framework/engine";
14
14
  import type { MetricsHandle } from "@cosmicdrift/kumiko-framework/observability";
15
15
  import { INBOUND_MAIL_PROVIDER_EXTENSION, type InboundMailAccountStatus } from "./constants.ts";
16
- import type { OutboundMailMessage, OutboundMailSent } from "./outbound-mail.ts";
16
+ import type { InboundOAuthFlow } from "./inbound-oauth-flow.ts";
17
+ import type { OutboundAttachment, OutboundMailMessage, ProviderSentMail } from "./outbound-mail.ts";
18
+ import type { SenderAuthentication } from "./sender-auth.ts";
17
19
 
18
20
  /**
19
21
  * Slim context for provider plugins.
@@ -59,8 +61,29 @@ export type MailAccountRecord = {
59
61
  /** Postfach-Adresse, Plaintext (decrypted). */
60
62
  readonly address: string;
61
63
  readonly displayName: string;
64
+ /** The real status; the supervisor syncs active and degraded mailboxes. */
62
65
  readonly status: InboundMailAccountStatus;
63
66
  readonly watchState: string;
67
+ /** authserv-id of the mailbox's own server for sender verification; absent or null = none set. */
68
+ readonly trustedAuthservId?: string | null;
69
+ };
70
+
71
+ export type RawInboundAttachment = {
72
+ readonly fileName: string;
73
+ readonly mimeType: string;
74
+ readonly data: Uint8Array;
75
+ };
76
+
77
+ /** An attachment that was not stored. Carries no file name: the reason is shown to operators, not the content. */
78
+ export type SkippedInboundAttachment = {
79
+ readonly mimeType: string;
80
+ readonly size: number;
81
+ readonly reason:
82
+ | "too_large"
83
+ | "mail_too_large"
84
+ | "rejected"
85
+ | "no_file_storage"
86
+ | "nesting_too_deep";
64
87
  };
65
88
 
66
89
  /**
@@ -89,9 +112,17 @@ export type RawInboundMessage = {
89
112
  readonly snippet: string;
90
113
  /** ISO instant — INTERNALDATE or receivedDateTime, not ingest time. */
91
114
  readonly receivedAtIso: string;
92
- /** Raw MIME; null in snippet-only mode. The Foundation persists it through
115
+ /** Raw MIME; null when the provider has no original to keep. The Foundation persists it through
93
116
  * file-foundation and writes bodyRef, so the event store remains blob-free. */
94
117
  readonly rawMime: Uint8Array | null;
118
+ /** Full plaintext (derived from the HTML for HTML-only mails). Optional so providers
119
+ * predating the field keep compiling; the supervisor treats absent as empty. */
120
+ readonly text?: string;
121
+ readonly html?: string | null;
122
+ readonly attachments?: readonly RawInboundAttachment[];
123
+ readonly skippedAttachments?: readonly SkippedInboundAttachment[];
124
+ /** What the mailbox's own server verified about the sender. Absent counts as unknown, never as pass. */
125
+ readonly senderAuthentication?: SenderAuthentication | null;
95
126
  /** Generic scope hint ("inbox", folder name), with no application semantics. */
96
127
  readonly scope: string;
97
128
  };
@@ -101,55 +132,39 @@ export type RawInboundMessage = {
101
132
  * store_mail_sync_cursors, interpretiert ihn nie. */
102
133
  export type SyncCursorPayload = Readonly<Record<string, unknown>>;
103
134
 
135
+ /** A message the provider could not turn into a RawInboundMessage; the cursor moves past it. */
136
+ export type RejectedInboundMessage = {
137
+ readonly providerMessageId: string;
138
+ readonly reason: "unparseable";
139
+ /** The original, when the provider has it; kept so the quarantined mail stays visible and retryable. */
140
+ readonly rawMime?: Uint8Array;
141
+ /** Envelope headers the provider could read without parsing the body. */
142
+ readonly headers?: {
143
+ readonly from: string;
144
+ readonly to: readonly string[];
145
+ readonly cc: readonly string[];
146
+ readonly subject: string;
147
+ readonly messageIdHeader: string | null;
148
+ readonly receivedAtIso: string;
149
+ };
150
+ };
151
+
152
+ export type InboundContentFetchResult = {
153
+ readonly messages: readonly RawInboundMessage[];
154
+ /** Not on the server (anymore) or from a previous UIDVALIDITY. */
155
+ readonly missing: readonly string[];
156
+ readonly rejected: readonly RejectedInboundMessage[];
157
+ };
158
+
104
159
  export type InboundFetchResult = {
105
160
  readonly messages: readonly RawInboundMessage[];
161
+ readonly rejected?: readonly RejectedInboundMessage[];
106
162
  readonly nextCursor: SyncCursorPayload;
107
163
  /** true ⇒ the Foundation calls fetch again in the same poll tick
108
164
  * (pagination) until false or the maxMessages budget is exhausted. */
109
165
  readonly hasMore: boolean;
110
166
  };
111
167
 
112
- export type OAuthTokenSet = {
113
- readonly accessToken: string;
114
- /** ISO-Instant. TTL typisch ~1h ⇒ Foundation refresht before-poll. */
115
- readonly expiresAt: string;
116
- readonly refreshToken?: string;
117
- readonly scopesGranted: readonly string[];
118
- };
119
-
120
- /**
121
- * OAuth portion of the contract — OAuth providers only; imap in password
122
- * mode omits `oauth`. The Foundation provides the routes
123
- * (`/inbound-mail/connect`, `/inbound-mail/oauth/callback`) generically;
124
- * providers MUST NOT register their own routes.
125
- */
126
- export type InboundOAuthFlow = {
127
- /** Scopes: request receive AND send in a SINGLE consent; re-consent is costly. */
128
- readonly scopes: {
129
- readonly receive: readonly string[];
130
- readonly send?: readonly string[];
131
- };
132
- readonly buildAuthorizeUrl: (
133
- ctx: InboundMailContext,
134
- p: { readonly state: string; readonly redirectUri: string },
135
- ) => Promise<string>;
136
- readonly exchangeCode: (
137
- ctx: InboundMailContext,
138
- p: { readonly code: string; readonly redirectUri: string },
139
- ) => Promise<OAuthTokenSet>;
140
- readonly refreshAccessToken: (
141
- ctx: InboundMailContext,
142
- account: MailAccountRecord,
143
- refreshToken: string,
144
- /** The foundation aborts `signal` after a hard timeout while holding a
145
- * per-account lock (a pool connection is pinned meanwhile). Providers
146
- * MUST pass it to their token-endpoint fetch; the foundation awaits the
147
- * provider's result and never drops a late one, because that could lose
148
- * a rotated refresh token. */
149
- opts?: { readonly signal?: AbortSignal },
150
- ) => Promise<OAuthTokenSet>;
151
- };
152
-
153
168
  /**
154
169
  * Inbound-mail provider plugin contract. Each provider feature
155
170
  * (inbound-provider-imap, -m365-graph, -gmail-rest) registers an
@@ -183,8 +198,19 @@ export type InboundMailProviderPlugin = {
183
198
  readonly filterUnknownMessageIds?: (
184
199
  providerMessageIds: readonly string[],
185
200
  ) => Promise<ReadonlySet<string>>;
201
+ /** Aborted when the supervisor gives up on this account's sync (per-account deadline). A
202
+ * provider that can cancel closes its connection then; one that cannot is abandoned and
203
+ * its late result is dropped. */
204
+ readonly signal?: AbortSignal;
186
205
  },
187
206
  ) => Promise<InboundFetchResult>;
207
+ /** Re-downloads already imported messages that were stored as snippet only. */
208
+ readonly fetchContent?: (
209
+ ctx: InboundMailContext,
210
+ account: MailAccountRecord,
211
+ providerMessageIds: readonly string[],
212
+ opts?: { readonly accessToken?: string; readonly signal?: AbortSignal },
213
+ ) => Promise<InboundContentFetchResult>;
188
214
  /** Nur OAuth-Provider. */
189
215
  readonly oauth?: InboundOAuthFlow;
190
216
  /** Sends from the mailbox address through the customer's own server. Throws
@@ -193,7 +219,8 @@ export type InboundMailProviderPlugin = {
193
219
  ctx: InboundMailContext,
194
220
  account: MailAccountRecord,
195
221
  message: OutboundMailMessage,
196
- ) => Promise<OutboundMailSent>;
222
+ attachments: readonly OutboundAttachment[],
223
+ ) => Promise<ProviderSentMail>;
197
224
  /** Optional live push (IMAP IDLE, later Graph/Gmail webhooks).
198
225
  * Returns stop(). Polling remains reconciliation: watch optimizes
199
226
  * latency but is never the correctness anchor. `onError` reports a dead
@@ -203,7 +230,11 @@ export type InboundMailProviderPlugin = {
203
230
  ctx: InboundMailContext,
204
231
  account: MailAccountRecord,
205
232
  handlers: {
206
- readonly onMessages: (msgs: readonly RawInboundMessage[]) => Promise<void>;
233
+ /** `rejected`: mails the provider could not parse, quarantined right away instead of at the next poll. */
234
+ readonly onMessages: (
235
+ msgs: readonly RawInboundMessage[],
236
+ rejected?: readonly RejectedInboundMessage[],
237
+ ) => Promise<void>;
207
238
  readonly onError: (err: unknown) => void;
208
239
  },
209
240
  /** Same `accessToken` semantics as `fetch` opts; re-minted on every
@@ -232,63 +263,3 @@ declare module "@cosmicdrift/kumiko-framework/engine" {
232
263
  [INBOUND_MAIL_PROVIDER_EXTENSION]: InboundMailProviderPlugin;
233
264
  }
234
265
  }
235
-
236
- // =============================================================================
237
- // Typed errors handled uniformly by the Foundation during polling:
238
- // InboundAuthError → status=auth_error, no retry
239
- // InboundRateLimitError → abort poll, wait for the next cron tick
240
- // InboundCursorInvalidError → reset cursor + full resync in the backfill
241
- // window; deduplication handles duplicates
242
- // InboundTransientError → job retry (retries: 3, exponential)
243
- // =============================================================================
244
- //
245
- // Use a `kind` discriminant instead of instanceof: plugins and the Foundation
246
- // may run in separate bundles (dual-package-hazard), and instanceof is
247
- // unreliable across realm or package-copy boundaries.
248
-
249
- export class InboundAuthError extends Error {
250
- readonly kind = "inbound-auth-error" as const;
251
- constructor(message: string) {
252
- super(message);
253
- this.name = "InboundAuthError";
254
- }
255
- }
256
-
257
- export class InboundRateLimitError extends Error {
258
- readonly kind = "inbound-rate-limit-error" as const;
259
- readonly retryAfterMs: number;
260
- constructor(message: string, retryAfterMs: number) {
261
- super(message);
262
- this.name = "InboundRateLimitError";
263
- this.retryAfterMs = retryAfterMs;
264
- }
265
- }
266
-
267
- export class InboundCursorInvalidError extends Error {
268
- readonly kind = "inbound-cursor-invalid-error" as const;
269
- constructor(message: string) {
270
- super(message);
271
- this.name = "InboundCursorInvalidError";
272
- }
273
- }
274
-
275
- export class InboundTransientError extends Error {
276
- readonly kind = "inbound-transient-error" as const;
277
- constructor(message: string) {
278
- super(message);
279
- this.name = "InboundTransientError";
280
- }
281
- }
282
-
283
- export function isInboundAuthError(e: unknown): e is InboundAuthError {
284
- return e instanceof Error && "kind" in e && e.kind === "inbound-auth-error";
285
- }
286
- export function isInboundRateLimitError(e: unknown): e is InboundRateLimitError {
287
- return e instanceof Error && "kind" in e && e.kind === "inbound-rate-limit-error";
288
- }
289
- export function isInboundCursorInvalidError(e: unknown): e is InboundCursorInvalidError {
290
- return e instanceof Error && "kind" in e && e.kind === "inbound-cursor-invalid-error";
291
- }
292
- export function isInboundTransientError(e: unknown): e is InboundTransientError {
293
- return e instanceof Error && "kind" in e && e.kind === "inbound-transient-error";
294
- }
@@ -0,0 +1,33 @@
1
+ import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
3
+ import { wakeMailAccount } from "./account-wake.ts";
4
+ import { InboundMailAccountStatuses } from "./constants.ts";
5
+ import { MAIL_ACCOUNT_CONNECTED_EVENT_QN, MAIL_ACCOUNT_UPDATED_EVENT_QN } from "./events.ts";
6
+
7
+ const SUPERVISOR_AUDIT_REASON = "watch_supervisor";
8
+
9
+ // Per-instance consumer: every process sees every account event, so the supervisor running in that
10
+ // process syncs a freshly connected, reconnected or re-activated mailbox immediately. Supervisor-made
11
+ // updates (watch state, health) are skipped, otherwise its own bookkeeping would trigger syncs.
12
+ // It only reaches an in-process registry and never writes to the database.
13
+ export function registerMailAccountWakeConsumer(r: FeatureRegistrar<"inbound-mail-foundation">) {
14
+ const wakeIfActive = (
15
+ event: Parameters<Parameters<typeof r.multiStreamProjection>[0]["apply"][string]>[0],
16
+ ): void => {
17
+ // skip: only an account that is active again needs an immediate sync.
18
+ if (propertyOf(event.payload, "status") !== InboundMailAccountStatuses.active) return;
19
+ // skip: the supervisor's own bookkeeping must not trigger another sync.
20
+ if (event.metadata.headers?.["reason"] === SUPERVISOR_AUDIT_REASON) return;
21
+ wakeMailAccount(event.tenantId, event.aggregateId);
22
+ };
23
+ r.multiStreamProjection({
24
+ name: "mail-account-wake",
25
+ delivery: "per-instance",
26
+ startFrom: "now",
27
+ runIn: "both",
28
+ apply: {
29
+ [MAIL_ACCOUNT_CONNECTED_EVENT_QN]: async (event) => wakeIfActive(event),
30
+ [MAIL_ACCOUNT_UPDATED_EVENT_QN]: async (event) => wakeIfActive(event),
31
+ },
32
+ });
33
+ }
@@ -0,0 +1,68 @@
1
+ import { describeError } from "./describe-error.ts";
2
+ import {
3
+ type InFlightSyncs,
4
+ type PollAccountOptions,
5
+ type PollAccountResult,
6
+ runExclusivePerAccount,
7
+ } from "./sync-account-now.ts";
8
+ import type { SyncableAccount } from "./syncable-accounts.ts";
9
+ import type { InboundMailProviderPlugin } from "./types.ts";
10
+
11
+ export type WatchCatchUp = {
12
+ /** Fire-and-forget; failures are logged and never stop the watch. */
13
+ readonly schedule: (account: SyncableAccount, plugin: InboundMailProviderPlugin) => void;
14
+ readonly settled: () => Promise<unknown>;
15
+ };
16
+
17
+ /**
18
+ * A watch only sees mail that arrives after its mailbox is open, so mail delivered between a
19
+ * poll's fetch and the watch start would wait for the next tick. One poll that starts after the
20
+ * watch is open closes that gap for every provider.
21
+ */
22
+ export function createWatchCatchUp(deps: {
23
+ readonly inFlight: InFlightSyncs;
24
+ readonly pollAccount: (
25
+ account: SyncableAccount,
26
+ plugin: InboundMailProviderPlugin,
27
+ options: PollAccountOptions,
28
+ ) => Promise<PollAccountResult>;
29
+ readonly isRunning: () => boolean;
30
+ readonly log: (line: string) => void;
31
+ }): WatchCatchUp {
32
+ const pending = new Set<Promise<void>>();
33
+
34
+ async function catchUp(
35
+ account: SyncableAccount,
36
+ plugin: InboundMailProviderPlugin,
37
+ ): Promise<void> {
38
+ // A sync running now may have fetched before the mailbox was open, so joining it would not close the gap.
39
+ for (
40
+ let running = deps.inFlight.get(account.id);
41
+ running;
42
+ running = deps.inFlight.get(account.id)
43
+ ) {
44
+ await running.catch(() => {});
45
+ }
46
+ // skip: the supervisor stopped while the sync ran.
47
+ if (!deps.isRunning()) return;
48
+ await runExclusivePerAccount(deps.inFlight, account.id, async () => {
49
+ await deps.pollAccount(account, plugin, {});
50
+ });
51
+ }
52
+
53
+ return {
54
+ schedule(account, plugin) {
55
+ const task: Promise<void> = catchUp(account, plugin)
56
+ .catch((err) =>
57
+ deps.log(
58
+ `inbound-mail: watch catch-up for account ${account.id} failed: ${describeError(err)}`,
59
+ ),
60
+ )
61
+ .finally(() => {
62
+ pending.delete(task);
63
+ });
64
+ pending.add(task);
65
+ },
66
+ settled: () => Promise.allSettled([...pending]),
67
+ };
68
+ }
@@ -0,0 +1,124 @@
1
+ import type { DistributedLock } from "@cosmicdrift/kumiko-framework/pipeline";
2
+ import { describeError } from "./describe-error.ts";
3
+ import type { MailAccountRecord } from "./types.ts";
4
+ import type { WatcherState, WatchLeaseResult } from "./watch-supervisor-types.ts";
5
+
6
+ export type WatchLease = {
7
+ readonly acquire: (account: MailAccountRecord, state: WatcherState) => Promise<WatchLeaseResult>;
8
+ };
9
+
10
+ // The cross-worker claim on one account's watch: acquired before plugin.watch(), renewed by a
11
+ // heartbeat for as long as the watcher state exists, and given up locally once it is lost.
12
+ export function createWatchLease(deps: {
13
+ readonly lock: DistributedLock | undefined;
14
+ readonly ttlSeconds: number;
15
+ readonly renewIntervalMs: number;
16
+ readonly watchers: ReadonlyMap<string, WatcherState>;
17
+ readonly isRunning: () => boolean;
18
+ readonly stopWatcher: (accountId: string) => Promise<void>;
19
+ readonly log: (line: string) => void;
20
+ }): WatchLease {
21
+ const { lock, ttlSeconds, renewIntervalMs, watchers, isRunning, stopWatcher, log } = deps;
22
+
23
+ // Heartbeat for a held watch lease. Independent of the connection's own
24
+ // lifecycle — it keeps renewing across reconnect backoff, not just while
25
+ // `plugin.watch()` is actually connected, so a flaky IMAP link doesn't
26
+ // make this worker lose the account to a peer mid-backoff.
27
+ function scheduleRenew(
28
+ account: MailAccountRecord,
29
+ state: WatcherState,
30
+ generation: number,
31
+ ): void {
32
+ state.renewTimer = setTimeout(() => {
33
+ void renewLeaseOnce(account, state, generation);
34
+ }, renewIntervalMs);
35
+ }
36
+
37
+ async function tearDownAfterRenewFailures(
38
+ account: MailAccountRecord,
39
+ state: WatcherState,
40
+ reason: string,
41
+ ): Promise<void> {
42
+ log(
43
+ `inbound-mail: watch lease ${reason} for account ${account.id} — tearing down local watcher`,
44
+ );
45
+ state.lockToken = null;
46
+ await stopWatcher(account.id);
47
+ }
48
+
49
+ async function renewLeaseOnce(
50
+ account: MailAccountRecord,
51
+ state: WatcherState,
52
+ generation: number,
53
+ ): Promise<void> {
54
+ state.renewTimer = null;
55
+ // skip: supervisor stopped, generation superseded, or lease already lost — stale timer fire, nothing to renew.
56
+ if (!isRunning() || state.generation !== generation || !state.lockToken || !lock) return;
57
+ let renewed: boolean;
58
+ try {
59
+ renewed = await lock.renew(account.id, state.lockToken, ttlSeconds);
60
+ } catch (err) {
61
+ log(
62
+ `inbound-mail: watch lease renew for account ${account.id} failed: ${describeError(err)}`,
63
+ );
64
+ state.renewFailures += 1;
65
+ // After failures spanning a full lease TTL, tear down — otherwise a
66
+ // peer can acquire the expired key while we still hold an IDLE conn.
67
+ if (state.renewFailures * renewIntervalMs >= ttlSeconds * 1000) {
68
+ await tearDownAfterRenewFailures(account, state, "renew failed across TTL");
69
+ // skip: connection torn down, no reschedule
70
+ return;
71
+ }
72
+ rescheduleIfStillHeld(account, state, generation);
73
+ // skip: renew failed but TTL not yet exhausted, retry already scheduled
74
+ return;
75
+ }
76
+ state.renewFailures = 0;
77
+ if (!renewed) {
78
+ await tearDownAfterRenewFailures(account, state, "lost");
79
+ // skip: lease lost and torn down, nothing to reschedule
80
+ return;
81
+ }
82
+ rescheduleIfStillHeld(account, state, generation);
83
+ }
84
+
85
+ function rescheduleIfStillHeld(
86
+ account: MailAccountRecord,
87
+ state: WatcherState,
88
+ generation: number,
89
+ ): void {
90
+ if (isRunning() && state.generation === generation && state.lockToken) {
91
+ scheduleRenew(account, state, generation);
92
+ }
93
+ }
94
+
95
+ // kumiko-lint-ignore complexity-budget yield-point stale-map check after acquire is intentional
96
+ async function acquire(
97
+ account: MailAccountRecord,
98
+ state: WatcherState,
99
+ ): Promise<WatchLeaseResult> {
100
+ if (!lock || state.lockToken) return "acquired";
101
+ // A throwing lock (Redis outage) reads as "not claimed": the next tick retries instead of leaving the start flag set.
102
+ const token = await lock.acquire(account.id, { ttlSeconds: ttlSeconds }).catch((err) => {
103
+ log(
104
+ `inbound-mail: watch lease acquire for account ${account.id} failed: ${describeError(err)}`,
105
+ );
106
+ return null;
107
+ });
108
+ if (!token) return "no-token";
109
+ // The acquire() await is a yield point: a concurrent stopWatcher()
110
+ // (e.g. from a lost-lease renewal on a different generation) may have
111
+ // already retired this exact state and removed it from the map. If so,
112
+ // committing the fresh token onto it would leak the lease — nothing
113
+ // would ever release it, blocking failover for the full TTL.
114
+ if (watchers.get(account.id) !== state) {
115
+ await lock.release(account.id, token);
116
+ return "stale";
117
+ }
118
+ state.lockToken = token;
119
+ scheduleRenew(account, state, state.generation);
120
+ return "acquired";
121
+ }
122
+
123
+ return { acquire };
124
+ }
@@ -0,0 +1,87 @@
1
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { DistributedLock } from "@cosmicdrift/kumiko-framework/pipeline";
3
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
+ import type { OAuthAccessTokenManager } from "./oauth-access-token.ts";
5
+ import type { InboundMailContext, MailAccountRecord, RawInboundMessage } from "./types.ts";
6
+
7
+ export type InboundMailSupervisorDeps = {
8
+ /** Slim context for provider calls (`registry` is required; `secrets` is
9
+ * used for provider credential reads). */
10
+ readonly providerCtx: InboundMailContext;
11
+ /** App database — direct reads from read_mail_accounts (account snapshot)
12
+ * and direct writes to store_mail_sync_cursors (unmanaged store). */
13
+ readonly db: DbConnection;
14
+ /** Standard dispatcher with SystemUser — carries ingest-message and
15
+ * update-account. */
16
+ readonly dispatchWrite: (args: {
17
+ readonly handlerQn: string;
18
+ readonly payload: unknown;
19
+ readonly tenantId: string;
20
+ }) => Promise<{
21
+ readonly isSuccess: boolean;
22
+ readonly data?: unknown;
23
+ readonly error?: unknown;
24
+ }>;
25
+ /** Optional override for where the raw MIME original is stored. Without it the ingest handler
26
+ * stores original and attachments itself through the tenant's file provider
27
+ * (bodyRef = "" here lets it pick the deterministic key). */
28
+ readonly storeBody?: (account: MailAccountRecord, msg: RawInboundMessage) => Promise<string>;
29
+ readonly pollIntervalMs?: number;
30
+ /** Clock for the failure-count bucket of a tick; tests inject a fake one. */
31
+ readonly now?: () => Temporal.Instant;
32
+ readonly backfillWindowDays?: number;
33
+ readonly maxMessagesPerPoll?: number;
34
+ /** Accounts synced side by side within one tick. Default 4. */
35
+ readonly pollConcurrency?: number;
36
+ /** Longest a single account's sync may run before the supervisor abandons it as a failed poll.
37
+ * Default 10 minutes. */
38
+ readonly accountPollDeadlineMs?: number;
39
+ readonly watchBackoffInitialMs?: number;
40
+ readonly watchBackoffMaxMs?: number;
41
+ /** Coordinates the IMAP watch (not the poll) across worker processes:
42
+ * before `plugin.watch()`, ensureWatcher claims a TTL lease for the
43
+ * account via `lock.acquire` and renews it via `lock.renew` while the
44
+ * watcher lives. Omit for single-process deployments — every active
45
+ * account is watched locally, same as before. */
46
+ readonly lock?: DistributedLock;
47
+ /** Access-token lifecycle for OAuth accounts. Defaults to a manager built
48
+ * from `db` + `providerCtx.secrets`; pass the instance shared with
49
+ * `createInboundMailConnectRoutes` so a fresh connect primes this cache. */
50
+ readonly oauthTokens?: OAuthAccessTokenManager;
51
+ /** TTL (seconds) for the per-account watch lease. Default 90s, renewed
52
+ * every ttl/3. Only used when `lock` is set. */
53
+ readonly watchLeaseTtlSeconds?: number;
54
+ readonly log?: (line: string) => void;
55
+ };
56
+
57
+ export type WatcherState = {
58
+ stop: (() => Promise<void>) | null;
59
+ backoffMs: number;
60
+ restartTimer: ReturnType<typeof setTimeout> | null;
61
+ /** Incremented by stop() to prevent a delayed restart from reviving
62
+ * an already stopped watcher. */
63
+ generation: number;
64
+ /** Token from `deps.lock.acquire` while this worker owns the account's
65
+ * watch lease; null when unclaimed (no `deps.lock`, or claim lost). */
66
+ lockToken: string | null;
67
+ renewTimer: ReturnType<typeof setTimeout> | null;
68
+ /** Consecutive renew() throws — tear down once failures span a full TTL. */
69
+ renewFailures: number;
70
+ /** True while plugin.watch() is in flight (re-entrancy guard). */
71
+ starting: boolean;
72
+ /** The provider reads the account it was handed at watch start; a changed trust setting
73
+ * only reaches it through a fresh watch. */
74
+ trustedAuthservId: string | null;
75
+ };
76
+
77
+ export type WatchLeaseResult = "acquired" | "no-token" | "stale";
78
+
79
+ export type InboundMailSupervisor = {
80
+ readonly start: () => Promise<void>;
81
+ /** Reconciliation run across all active accounts; also usable standalone
82
+ * for tests and manual operational triggers. */
83
+ readonly pollOnce: () => Promise<void>;
84
+ /** Syncs one account immediately (connect/reconnect wake-up); joins a sync already running for it. */
85
+ readonly syncAccountNow: (accountId: string) => Promise<void>;
86
+ readonly stop: () => Promise<void>;
87
+ };