@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.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +15 -5
- package/packages/bundled-features/src/account-security/README.md +1 -1
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/feature.ts +9 -1
- package/packages/bundled-features/src/audit/event-classification.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
- package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
- package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
- package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
- package/packages/bundled-features/src/cap-overview/types.ts +2 -0
- package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
- package/packages/bundled-features/src/files/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +143 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
- package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
- package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
- package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -2
- package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +93 -15
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
- package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
- package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
- package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
- package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +238 -89
- package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
- package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
- package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
- package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
- package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +36 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +124 -41
- package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +61 -136
- package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +9 -0
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
- package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
- package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +4 -6
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +75 -45
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +19 -8
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +203 -109
- package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
- package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
- package/packages/bundled-features/src/inbound-provider-imap/watch-mailbox.ts +109 -0
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
- package/packages/bundled-features/src/jobs/changes.json +18 -0
- package/packages/bundled-features/src/managed-pages/changes.json +7 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +7 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +6 -4
- package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +2 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-guard.ts +61 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-body-issues.ts +26 -0
- package/packages/bundled-features/src/managed-pages/handlers/page-crud.ts +17 -10
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +15 -4
- package/packages/bundled-features/src/managed-pages/render-page-body.ts +28 -0
- package/packages/bundled-features/src/managed-pages/screens/page-screens.ts +6 -2
- package/packages/bundled-features/src/managed-pages/seeding.ts +13 -2
- package/packages/bundled-features/src/managed-pages/table.ts +6 -2
- package/packages/bundled-features/src/managed-pages/web/client-plugin.tsx +6 -4
- package/packages/bundled-features/src/managed-pages/web/constants.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +14 -0
- package/packages/bundled-features/src/managed-pages/web/lossy-markdown.ts +71 -0
- package/packages/bundled-features/src/managed-pages/web/markdown-to-document.ts +27 -0
- package/packages/bundled-features/src/managed-pages/web/page-body-section.tsx +73 -0
- package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
- package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
- package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
- package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
- package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
- package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
- package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
- package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
- package/packages/bundled-features/src/template-resolver/README.md +2 -2
- package/packages/bundled-features/src/template-resolver/changes.json +20 -0
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +16 -16
- package/packages/bundled-features/src/template-resolver/feature.ts +14 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +24 -6
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +32 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +9 -3
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +13 -480
- package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +105 -0
- package/packages/bundled-features/src/template-resolver/web/collection-lookup.ts +33 -0
- package/packages/bundled-features/src/template-resolver/web/constants.ts +3 -0
- package/packages/bundled-features/src/template-resolver/web/content-field.tsx +61 -0
- package/packages/bundled-features/src/template-resolver/web/content-tree-provider.ts +256 -0
- package/packages/bundled-features/src/template-resolver/web/document-reference-name.ts +12 -0
- package/packages/bundled-features/src/template-resolver/web/document-schema.ts +3 -11
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +9 -2
- package/packages/bundled-features/src/template-resolver/web/legacy-variable-text.ts +30 -0
- package/packages/bundled-features/src/template-resolver/web/tenant-override.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/text-block-editor.tsx +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-editor-context.ts +42 -0
- package/packages/bundled-features/src/template-resolver/web/use-collection-entry-create.ts +92 -0
- package/packages/bundled-features/src/template-resolver/web/use-content-write.ts +81 -0
- package/packages/bundled-features/src/template-resolver/web/use-text-block-editor.ts +127 -0
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +9 -1
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +89 -40
- package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
- package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/client.tsx +8 -1
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +17 -2
- package/samples/apps/use-all-bundled/src/run-config.ts +19 -11
- package/samples/recipes/cross-feature-events/README.md +10 -0
- package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
- package/samples/recipes/dashboard-stats/README.md +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
- package/samples/recipes/env-composition/README.md +2 -2
- package/samples/recipes/env-composition/src/env.ts +2 -2
- package/samples/recipes/inbound-mail-triage/README.md +8 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
- package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
- package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
- package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
- package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
- package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
- 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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|