@cosmicdrift/kumiko-samples 0.392.2 → 0.394.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +15 -5
  3. package/packages/bundled-features/src/account-security/README.md +1 -1
  4. package/packages/bundled-features/src/account-security/changes.json +6 -0
  5. package/packages/bundled-features/src/account-security/feature.ts +9 -1
  6. package/packages/bundled-features/src/audit/event-classification.ts +1 -0
  7. package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
  8. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
  9. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
  10. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
  11. package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
  12. package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
  13. package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
  14. package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
  15. package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
  16. package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
  17. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
  18. package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
  19. package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
  20. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
  21. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
  22. package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
  23. package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
  24. package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
  25. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
  26. package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
  27. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
  28. package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
  29. package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
  30. package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
  31. package/packages/bundled-features/src/cap-overview/changes.json +6 -0
  32. package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
  33. package/packages/bundled-features/src/cap-overview/types.ts +2 -0
  34. package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
  35. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
  36. package/packages/bundled-features/src/files/changes.json +18 -0
  37. package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
  38. package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
  39. package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
  40. package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
  41. package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
  42. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +155 -0
  43. package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
  44. package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
  45. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
  46. package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
  47. package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
  48. package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
  49. package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
  50. package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
  51. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
  52. package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
  53. package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
  54. package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
  55. package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
  56. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
  57. package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
  58. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
  59. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
  60. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
  61. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
  62. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
  63. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
  64. package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
  65. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
  66. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
  67. package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
  68. package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
  69. package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
  70. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
  71. package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
  72. package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
  73. package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
  74. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
  75. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -1
  76. package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
  77. package/packages/bundled-features/src/inbound-mail-foundation/index.ts +94 -15
  78. package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
  79. package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
  80. package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
  81. package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
  82. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
  83. package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
  84. package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
  85. package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
  89. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
  90. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
  91. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
  92. package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
  93. package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
  94. package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
  95. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
  96. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +239 -79
  97. package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
  98. package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
  99. package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
  100. package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
  101. package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
  102. package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
  103. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
  104. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
  105. package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
  106. package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
  107. package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
  108. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
  109. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
  110. package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
  111. package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
  112. package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
  113. package/packages/bundled-features/src/inbound-provider-imap/changes.json +42 -0
  114. package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +155 -63
  115. package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
  116. package/packages/bundled-features/src/inbound-provider-imap/constants.ts +14 -1
  117. package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
  118. package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
  119. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +75 -44
  120. package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
  121. package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +28 -2
  122. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
  123. package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
  124. package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
  125. package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
  126. package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +45 -0
  127. package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +103 -0
  128. package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +49 -0
  129. package/packages/bundled-features/src/inbound-provider-imap/screens.ts +209 -42
  130. package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
  131. package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
  132. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
  133. package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
  134. package/packages/bundled-features/src/jobs/changes.json +18 -0
  135. package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -0
  136. package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
  137. package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
  138. package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
  139. package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
  140. package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
  141. package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
  142. package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
  143. package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
  144. package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
  145. package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
  146. package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
  147. package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
  148. package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
  149. package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
  150. package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
  151. package/packages/bundled-features/src/template-resolver/changes.json +12 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
  153. package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +25 -0
  155. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +218 -117
  156. package/packages/bundled-features/src/template-resolver/web/i18n.ts +1 -2
  157. package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
  158. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
  159. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
  160. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
  161. package/samples/apps/use-all-bundled/feature-manifest.json +89 -39
  162. package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
  163. package/samples/apps/use-all-bundled/package.json +1 -0
  164. package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
  165. package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
  166. package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
  167. package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
  168. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
  169. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -1
  170. package/samples/apps/use-all-bundled/src/run-config.ts +7 -5
  171. package/samples/recipes/cross-feature-events/README.md +10 -0
  172. package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
  173. package/samples/recipes/dashboard-stats/README.md +8 -0
  174. package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
  175. package/samples/recipes/env-composition/README.md +2 -2
  176. package/samples/recipes/env-composition/src/env.ts +2 -2
  177. package/samples/recipes/inbound-mail-triage/README.md +12 -0
  178. package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
  179. package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
  180. package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
  181. package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
  182. package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
  183. package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
  184. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
  185. package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
  186. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
@@ -0,0 +1,137 @@
1
+ // Reloads the content of mails that were imported as snippet only (event version 1, before full text
2
+ // storage existed). Runs after a successful poll with a small per-account budget, so a large backlog
3
+ // is worked off over several ticks without slowing the regular sync. "unavailable" is terminal:
4
+ // mails that are gone from the server stop being candidates.
5
+
6
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
7
+ import { selectMany } from "@cosmicdrift/kumiko-framework/db";
8
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
9
+ import { inboundMessageAggregateId } from "./aggregate-id.ts";
10
+ import { InboundMailFoundationHandlers } from "./constants.ts";
11
+ import { seenMessageTable } from "./entities.ts";
12
+ import { normalizeRawInboundMessage } from "./normalize-inbound-message.ts";
13
+ import { inboundMessagesProjectionTable } from "./projection.ts";
14
+ import type { DispatchWrite } from "./supervisor-ingest.ts";
15
+ import type { InboundMailContext, InboundMailProviderPlugin, MailAccountRecord } from "./types.ts";
16
+
17
+ export const CONTENT_REFETCH_BUDGET_PER_TICK = 50;
18
+ const SEEN_PAGE_SIZE = 250;
19
+ const SEEN_MAX_PAGES = 4;
20
+
21
+ type SeenRow = { readonly providerMessageId: string; readonly seenAt: Temporal.Instant };
22
+ type MessageRow = {
23
+ readonly id: string;
24
+ readonly bodyRef?: string | null;
25
+ readonly contentState?: string | null;
26
+ };
27
+
28
+ function needsContent(row: MessageRow): boolean {
29
+ const bodyMissing = row.bodyRef === undefined || row.bodyRef === null || row.bodyRef === "";
30
+ return bodyMissing && (row.contentState == null || row.contentState === "snippet");
31
+ }
32
+
33
+ async function findSnippetOnlyMessageIds(
34
+ db: DbConnection,
35
+ account: MailAccountRecord,
36
+ windowDays: number,
37
+ ): Promise<string[]> {
38
+ const cutoff = Temporal.Now.instant().subtract({ hours: windowDays * 24 });
39
+ const found: string[] = [];
40
+ let before: Temporal.Instant | undefined;
41
+ for (
42
+ let page = 0;
43
+ page < SEEN_MAX_PAGES && found.length < CONTENT_REFETCH_BUDGET_PER_TICK;
44
+ page++
45
+ ) {
46
+ const seen = await selectMany<SeenRow>(
47
+ db,
48
+ seenMessageTable,
49
+ {
50
+ tenantId: account.tenantId,
51
+ accountId: account.id,
52
+ seenAt: before ? { gte: cutoff, lt: before } : { gte: cutoff },
53
+ },
54
+ {
55
+ fields: ["providerMessageId", "seenAt"],
56
+ orderBy: { col: "seenAt", direction: "desc" },
57
+ limit: SEEN_PAGE_SIZE,
58
+ },
59
+ );
60
+ if (seen.length === 0) break;
61
+ const aggregateToProvider = new Map(
62
+ seen.map((row) => [
63
+ inboundMessageAggregateId(account.id, row.providerMessageId),
64
+ row.providerMessageId,
65
+ ]),
66
+ );
67
+ const rows = await selectMany<MessageRow>(
68
+ db,
69
+ inboundMessagesProjectionTable,
70
+ { tenantId: account.tenantId, id: { in: [...aggregateToProvider.keys()] } },
71
+ { fields: ["id", "bodyRef", "contentState"] },
72
+ );
73
+ for (const row of rows) {
74
+ const providerMessageId = aggregateToProvider.get(row.id);
75
+ if (providerMessageId !== undefined && needsContent(row)) found.push(providerMessageId);
76
+ }
77
+ const oldest = seen[seen.length - 1];
78
+ if (seen.length < SEEN_PAGE_SIZE || !oldest) break;
79
+ before = oldest.seenAt;
80
+ }
81
+ return found.slice(0, CONTENT_REFETCH_BUDGET_PER_TICK);
82
+ }
83
+
84
+ export async function refetchSnippetOnlyMessages(args: {
85
+ readonly db: DbConnection;
86
+ readonly dispatchWrite: DispatchWrite;
87
+ readonly providerCtx: InboundMailContext;
88
+ readonly account: MailAccountRecord;
89
+ readonly plugin: InboundMailProviderPlugin;
90
+ readonly windowDays: number;
91
+ readonly accessToken?: string | undefined;
92
+ readonly signal?: AbortSignal | undefined;
93
+ }): Promise<void> {
94
+ const { account, plugin, dispatchWrite } = args;
95
+ // skip: the provider cannot reload content.
96
+ if (!plugin.fetchContent) return;
97
+ const ids = await findSnippetOnlyMessageIds(args.db, account, args.windowDays);
98
+ // skip: no snippet-only mail to reload.
99
+ if (ids.length === 0) return;
100
+ const result = await plugin.fetchContent(
101
+ args.providerCtx,
102
+ account,
103
+ ids,
104
+ args.accessToken !== undefined || args.signal !== undefined
105
+ ? {
106
+ ...(args.accessToken !== undefined && { accessToken: args.accessToken }),
107
+ ...(args.signal !== undefined && { signal: args.signal }),
108
+ }
109
+ : undefined,
110
+ );
111
+ const attach = (providerMessageId: string, content: Record<string, unknown>) =>
112
+ dispatchWrite({
113
+ handlerQn: InboundMailFoundationHandlers.attachMessageContent,
114
+ tenantId: account.tenantId,
115
+ payload: { accountId: account.id, providerMessageId, ...content },
116
+ });
117
+ for (const raw of result.messages) {
118
+ const msg = normalizeRawInboundMessage(raw);
119
+ if (msg.bodyText === "" && msg.rawMime == null && msg.bodyHtml === null) {
120
+ // Nothing usable came back: attach would leave the mail at "snippet", i.e. a candidate on
121
+ // every tick. "unavailable" is terminal.
122
+ await attach(msg.providerMessageId, { outcome: "unavailable" });
123
+ continue;
124
+ }
125
+ await attach(msg.providerMessageId, {
126
+ outcome: "fetched",
127
+ bodyText: msg.bodyText,
128
+ bodyTextTruncated: msg.bodyTextTruncated,
129
+ ...(msg.rawMime != null && { rawMime: msg.rawMime }),
130
+ ...(msg.bodyHtml !== null && { bodyHtml: msg.bodyHtml }),
131
+ attachments: msg.attachments,
132
+ skippedAttachments: msg.skippedAttachments,
133
+ });
134
+ }
135
+ const gone = [...result.missing, ...result.rejected.map((r) => r.providerMessageId)];
136
+ for (const providerMessageId of gone) await attach(providerMessageId, { outcome: "unavailable" });
137
+ }
@@ -0,0 +1,5 @@
1
+ // Provider errors can embed token or response data in `message`; logs carry
2
+ // only the error class.
3
+ export function describeError(err: unknown): string {
4
+ return err instanceof Error ? err.name : typeof err;
5
+ }
@@ -2,7 +2,10 @@ import { collectPiiSubjectFields } from "@cosmicdrift/kumiko-framework/crypto";
2
2
  import { deriveEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
3
3
  import {
4
4
  access,
5
+ createBooleanField,
5
6
  createEntity,
7
+ createFilesField,
8
+ createLongTextField,
6
9
  createNumberField,
7
10
  createTextField,
8
11
  createTimestampField,
@@ -64,7 +67,15 @@ export const mailAccountEntity = createEntity({
64
67
  personal: false,
65
68
  reason: "technical_reference",
66
69
  }),
70
+ // Code of InboundMailLastErrors, never provider error text. Null on rows from before the column.
71
+ lastError: createTextField({ maxLength: 50, personal: false, reason: "technical_reference" }),
67
72
  watchState: createTextField({ maxLength: 100, personal: false, reason: "technical_reference" }),
73
+ // Lower-case authserv-id of the mailbox's own server; null = none set.
74
+ trustedAuthservId: createTextField({
75
+ maxLength: 255,
76
+ personal: false,
77
+ reason: "technical_reference",
78
+ }),
68
79
  connectedAt: createTimestampField({ required: true }),
69
80
  },
70
81
  });
@@ -82,8 +93,7 @@ export const inboundMessageEntity = createEntity({
82
93
  reason: "technical_reference",
83
94
  }),
84
95
  // Scope is inherited from the account at ingest time: messages from a
85
- // personal mailbox are visible only to the owner (+ TenantAdmin),
86
- // without joining read_mail_accounts.
96
+ // personal mailbox are visible only to the owner, without joining read_mail_accounts.
87
97
  ownerUserId: createTextField({ maxLength: 36, personal: false, reason: "pseudonymous_fk" }),
88
98
  messageIdHeader: createTextField({
89
99
  required: true,
@@ -106,7 +116,40 @@ export const inboundMessageEntity = createEntity({
106
116
  receivedAt: createTimestampField({ required: true }),
107
117
  bodyRef: createTextField({ maxLength: 500, personal: false, reason: "technical_reference" }),
108
118
  scope: createTextField({ maxLength: 200, personal: false, reason: "technical_reference" }),
119
+ // Full plaintext, capped at 32_000 characters before encryption (ciphertext is ~2.3x plus envelope).
120
+ // Null on rows from before the column; list-messages never selects it.
121
+ bodyText: createLongTextField({ maxLength: 200_000, personal: "tenant", find: "none" }),
122
+ bodyTextTruncated: createBooleanField(),
123
+ // Bound to the message (entityType = the aggregate type), so the file guard answers with the
124
+ // mail's visibility. The fileRefs are bound at ingest, the projection copies the ids.
125
+ attachmentFileIds: createFilesField({ personal: false, reason: "technical_reference" }),
126
+ // JSON-stringified SkippedInboundAttachment[]: nullable JSON stays a text column
127
+ // (jsonb fields are NOT NULL), like to/cc.
128
+ skippedAttachments: createTextField({
129
+ maxLength: 20_000,
130
+ personal: false,
131
+ reason: "technical_reference",
132
+ }),
133
+ // JSON-stringified SenderAuthentication or "null"; a SQL null (rows from before the column) reads as null.
134
+ senderAuthentication: createTextField({
135
+ maxLength: 4000,
136
+ personal: false,
137
+ reason: "technical_reference",
138
+ }),
139
+ // "complete" | "snippet" | "unavailable"; null counts as "snippet".
140
+ contentState: createTextField({
141
+ maxLength: 20,
142
+ personal: false,
143
+ reason: "technical_reference",
144
+ }),
145
+ // One of INBOUND_CONTENT_UNAVAILABLE_REASONS; null while the content is there or not yet missing.
146
+ contentUnavailableReason: createTextField({
147
+ maxLength: 30,
148
+ personal: false,
149
+ reason: "technical_reference",
150
+ }),
109
151
  },
152
+ attachedFileAccess: "record-only",
110
153
  });
111
154
 
112
155
  /** Thread-Rollup. PK = aggregateId = mailThreadAggregateId(tenantId,
@@ -22,6 +22,7 @@
22
22
 
23
23
  import * as z from "zod";
24
24
  import { INBOUND_MAIL_FOUNDATION_FEATURE, InboundMailAccountStatuses } from "./constants.ts";
25
+ import { senderAuthenticationFieldSchema } from "./sender-auth.ts";
25
26
 
26
27
  // Aggregate types for the event store.
27
28
  export const MAIL_ACCOUNT_AGGREGATE_TYPE = "mail-account" as const;
@@ -35,6 +36,8 @@ export const MAIL_ACCOUNT_UPDATED_EVENT_SHORT = "mail-account-updated" as const;
35
36
  export const MAIL_ACCOUNT_DISCONNECTED_EVENT_SHORT = "mail-account-disconnected" as const;
36
37
  export const INBOUND_MESSAGE_RECEIVED_EVENT_SHORT = "inbound-message-received" as const;
37
38
  export const MAIL_THREAD_UPDATED_EVENT_SHORT = "mail-thread-updated" as const;
39
+ export const INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_SHORT =
40
+ "inbound-message-content-fetched" as const;
38
41
 
39
42
  export const MAIL_ACCOUNT_CONNECTED_EVENT_QN =
40
43
  `${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${MAIL_ACCOUNT_CONNECTED_EVENT_SHORT}` as const;
@@ -44,6 +47,8 @@ export const MAIL_ACCOUNT_DISCONNECTED_EVENT_QN =
44
47
  `${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${MAIL_ACCOUNT_DISCONNECTED_EVENT_SHORT}` as const;
45
48
  export const INBOUND_MESSAGE_RECEIVED_EVENT_QN =
46
49
  `${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${INBOUND_MESSAGE_RECEIVED_EVENT_SHORT}` as const;
50
+ export const INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_QN =
51
+ `${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${INBOUND_MESSAGE_CONTENT_FETCHED_EVENT_SHORT}` as const;
47
52
  export const MAIL_THREAD_UPDATED_EVENT_QN =
48
53
  `${INBOUND_MAIL_FOUNDATION_FEATURE}:event:${MAIL_THREAD_UPDATED_EVENT_SHORT}` as const;
49
54
 
@@ -76,9 +81,51 @@ export const mailAccountEventPayloadSchema = z.object({
76
81
  /** Provider-agnostic watch state ("idle", "watching", "backoff:3", ...)
77
82
  * — an operational signal only, not PII. */
78
83
  watchState: z.string().max(100),
84
+ /** Why the mailbox is degraded (InboundMailLastErrors code); null while healthy. A string rather
85
+ * than an enum, like authMethod, so a new code needs no upcaster. */
86
+ lastError: z.string().max(50).nullable(),
87
+ /** authserv-id (lower case) of the mailbox's own server; only an Authentication-Results header
88
+ * with exactly this id is believed (sender-auth.ts). null = not set, no sender verdicts. */
89
+ trustedAuthservId: z.string().max(255).nullable(),
79
90
  });
80
91
  export type MailAccountEventPayload = z.infer<typeof mailAccountEventPayloadSchema>;
81
92
 
93
+ export const INBOUND_CONTENT_STATES = ["complete", "snippet", "unavailable"] as const;
94
+ export type InboundContentState = (typeof INBOUND_CONTENT_STATES)[number];
95
+
96
+ /** Why a message has no content: held back by the quarantine (retry possible), no file storage wired, or gone from the server. */
97
+ export const INBOUND_CONTENT_UNAVAILABLE_REASONS = [
98
+ "quarantined",
99
+ "no_file_storage",
100
+ "gone_from_server",
101
+ ] as const;
102
+ export type InboundContentUnavailableReason = (typeof INBOUND_CONTENT_UNAVAILABLE_REASONS)[number];
103
+
104
+ export const skippedAttachmentSchema = z.object({
105
+ mimeType: z.string().max(255),
106
+ size: z.number().int().min(0),
107
+ reason: z.enum([
108
+ "too_large",
109
+ "mail_too_large",
110
+ "rejected",
111
+ "no_file_storage",
112
+ "nesting_too_deep",
113
+ ]),
114
+ });
115
+
116
+ // Shared by inbound-message-received (v2+) and inbound-message-content-fetched.
117
+ // bodyText is ciphertext: 32_000 characters become roughly 75 KB, hence the generous max.
118
+ const inboundMessageContentShape = {
119
+ bodyText: z.string().max(200_000),
120
+ bodyTextTruncated: z.boolean(),
121
+ /** file-foundation FileRef ids (plaintext uuids). Attachments outlive the mail: apps turn them
122
+ * into documents, so the retention sweep never deletes them. */
123
+ attachmentFileIds: z.array(z.uuid()).max(50),
124
+ skippedAttachments: z.array(skippedAttachmentSchema).max(200),
125
+ contentState: z.enum(INBOUND_CONTENT_STATES),
126
+ contentUnavailableReason: z.enum(INBOUND_CONTENT_UNAVAILABLE_REASONS).nullable(),
127
+ };
128
+
82
129
  // ============================================================================
83
130
  // inbound-message stream — exactly ONE received event per message
84
131
  // ============================================================================
@@ -110,15 +157,35 @@ export const inboundMessageEventPayloadSchema = z.object({
110
157
  /** ISO instant — receipt time reported by the provider (INTERNALDATE or
111
158
  * receivedDateTime), not ingest time. */
112
159
  receivedAtIso: z.string().min(1),
113
- /** file-foundation reference to the raw body (MIME). Empty when the app
114
- * is mounted without body persistence (snippet-only mode). */
160
+ /** file-foundation reference to the original (.eml, or .html for HTML-only mails). Empty for
161
+ * snippet-only mails and when no file storage is available. */
115
162
  bodyRef: z.string().max(500),
116
163
  /** Domain scope hint for downstream business processes — generic
117
164
  * ("inbox", folder name), with NO app-specific semantics. */
118
165
  scope: z.string().max(200),
166
+ /** SPF/DKIM/DMARC as reported by the mailbox's own server; null when there is no trustworthy
167
+ * statement. Not PII. */
168
+ senderAuthentication: senderAuthenticationFieldSchema,
169
+ ...inboundMessageContentShape,
119
170
  });
120
171
  export type InboundMessageEventPayload = z.infer<typeof inboundMessageEventPayloadSchema>;
121
172
 
173
+ // ============================================================================
174
+ // Content of a message that was imported as snippet only and re-downloaded later.
175
+ // Same aggregate as the received event; applies as an update of the projection row.
176
+ // ============================================================================
177
+ export const inboundMessageContentFetchedEventPayloadSchema = z.object({
178
+ /** Account and scope of the message, so a subscriber can route the event without loading the
179
+ * stream. Null on events stored before v3: the stream id cannot be reversed to the account. */
180
+ accountId: z.uuid().nullable(),
181
+ ownerUserId: z.string().max(36).nullable(),
182
+ bodyRef: z.string().max(500),
183
+ ...inboundMessageContentShape,
184
+ });
185
+ export type InboundMessageContentFetchedEventPayload = z.infer<
186
+ typeof inboundMessageContentFetchedEventPayloadSchema
187
+ >;
188
+
122
189
  // ============================================================================
123
190
  // mail-thread-Stream — Thread-Rollup pro (tenantId, threadKey)
124
191
  // ============================================================================