@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
@@ -0,0 +1,65 @@
1
+ import type { SignatureExtraRouteVerifyRequest } from "@cosmicdrift/kumiko-framework/api";
2
+ import { ExtraRouteRejection } from "@cosmicdrift/kumiko-framework/api";
3
+ import type { Context } from "hono";
4
+ import { errorJson } from "./connect-route-responses.ts";
5
+ import { normalizeMailAddress } from "./handlers/account-address.ts";
6
+ import type { OAuthCodeExchange } from "./inbound-oauth-flow.ts";
7
+ import { type OAuthStatePayload, verifyOAuthState } from "./oauth-state.ts";
8
+
9
+ export type VerifiedCallback = { readonly code: string; readonly state: OAuthStatePayload };
10
+
11
+ export function verifyCallbackRequest(
12
+ request: SignatureExtraRouteVerifyRequest,
13
+ stateSecret: string,
14
+ ): VerifiedCallback {
15
+ const code = request.query["code"] ?? "";
16
+ const rawState = request.query["state"] ?? "";
17
+ if (!code || !rawState) {
18
+ throw new ExtraRouteRejection(
19
+ 400,
20
+ { error: { code: "invalid_callback", message: "missing code or state" } },
21
+ "inbound-mail oauth callback missing code or state",
22
+ );
23
+ }
24
+ const verified = verifyOAuthState(rawState, stateSecret);
25
+ if (!verified.ok) {
26
+ throw new ExtraRouteRejection(
27
+ 400,
28
+ { error: { code: "invalid_state", message: `state rejected: ${verified.reason}` } },
29
+ `inbound-mail oauth callback state rejected: ${verified.reason}`,
30
+ );
31
+ }
32
+ return { code, state: verified.payload };
33
+ }
34
+
35
+ // The refresh token the account needs, or the response that refuses the exchange result.
36
+ export function refreshTokenOrRefusal(
37
+ c: Context,
38
+ state: OAuthStatePayload,
39
+ tokens: OAuthCodeExchange,
40
+ ): string | Response {
41
+ // Personal only: a shared mailbox is often an M365 shared mailbox the admin opens with their own
42
+ // login (delegated access). A token without access to it shows up as auth_error on the first sync.
43
+ if (
44
+ state.ownerUserId &&
45
+ normalizeMailAddress(tokens.accountAddress) !== normalizeMailAddress(state.mailbox)
46
+ ) {
47
+ return errorJson(
48
+ c,
49
+ 403,
50
+ "mailbox_address_mismatch",
51
+ `the account you signed in with is not ${state.mailbox}; sign in with that mailbox's account`,
52
+ );
53
+ }
54
+ // No refresh token, no server-side sync. Consent must request offline_access (or
55
+ // access_type=offline); the provider's oauth block owns the scopes.
56
+ if (!tokens.refreshToken) {
57
+ return errorJson(
58
+ c,
59
+ 502,
60
+ "no_refresh_token",
61
+ `provider "${state.providerKey}" returned no refresh token — check the offline-access scope in the provider's oauth.scopes`,
62
+ );
63
+ }
64
+ return tokens.refreshToken;
65
+ }
@@ -0,0 +1,26 @@
1
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import type { Context } from "hono";
3
+ import { InboundMailConnectErrors } from "./constants.ts";
4
+
5
+ const CONNECT_CONFLICT_MESSAGES: Readonly<Record<string, string>> = {
6
+ [InboundMailConnectErrors.addressAlreadyConnected.reason]:
7
+ "this mailbox address is already connected",
8
+ [InboundMailConnectErrors.addressDisconnected.reason]:
9
+ "this mailbox was disconnected before; reconnect it from the mailbox list instead",
10
+ };
11
+
12
+ export function connectFailureResponse(c: Context, error: unknown) {
13
+ const reason = propertyOf(propertyOf(error, "details"), "reason");
14
+ const conflict = typeof reason === "string" ? CONNECT_CONFLICT_MESSAGES[reason] : undefined;
15
+ if (typeof reason === "string" && conflict) return errorJson(c, 409, reason, conflict);
16
+ return errorJson(c, 502, "account_create_failed", JSON.stringify(error ?? {}));
17
+ }
18
+
19
+ export function errorJson(
20
+ c: Context,
21
+ status: 400 | 401 | 403 | 404 | 409 | 500 | 502,
22
+ code: string,
23
+ message: string,
24
+ ) {
25
+ return c.json({ error: { code, message } }, status);
26
+ }
@@ -13,30 +13,81 @@
13
13
  import type {
14
14
  ExtraRouteDefinition,
15
15
  SignatureExtraRouteDeps,
16
- SignatureExtraRouteVerifyRequest,
17
16
  UserExtraRouteDeps,
18
17
  } from "@cosmicdrift/kumiko-framework/api";
19
- import { ExtraRouteRejection, signatureRoute } from "@cosmicdrift/kumiko-framework/api";
18
+ import { signatureRoute } from "@cosmicdrift/kumiko-framework/api";
20
19
  import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
21
20
  import type { Context } from "hono";
22
21
  import {
22
+ refreshTokenOrRefusal,
23
+ type VerifiedCallback,
24
+ verifyCallbackRequest,
25
+ } from "./connect-callback-verify.ts";
26
+ import { connectFailureResponse, errorJson } from "./connect-route-responses.ts";
27
+ import {
28
+ INBOUND_MAIL_CONNECT_PATH,
23
29
  INBOUND_MAIL_FOUNDATION_FEATURE,
24
30
  InboundMailAuthMethods,
25
31
  InboundMailFoundationHandlers,
26
- inboundCredentialSecretKey,
27
32
  } from "./constants.ts";
33
+ import { isMailAdmin } from "./handlers/scope-visibility.ts";
34
+ import { readInboundMailRoles } from "./mail-roles.ts";
28
35
  import {
29
- inboundRefreshTokenSecretOptions,
30
36
  type OAuthAccessTokenManager,
37
+ storeOAuthCallbackCredential,
31
38
  } from "./oauth-access-token.ts";
32
- import { type OAuthStatePayload, signOAuthState, verifyOAuthState } from "./oauth-state.ts";
39
+ import { signOAuthState } from "./oauth-state.ts";
33
40
  import { resolveInboundProviderForKey } from "./provider-factory.ts";
34
41
  import type { InboundMailContext } from "./types.ts";
35
42
 
36
43
  const DEFAULT_STATE_TTL_MINUTES = 15;
37
- const DEFAULT_CONNECT_PATH = "/api/inbound-mail/connect";
38
44
  const DEFAULT_CALLBACK_PATH = "/inbound-mail/oauth/callback";
39
45
 
46
+ // connect-account leaves a reopened mailbox off: switched on before its token is stored, the
47
+ // supervisor would sync it with the old, failing credential.
48
+ async function reactivateIfReopened(
49
+ c: Context,
50
+ deps: SignatureExtraRouteDeps,
51
+ tenantId: string,
52
+ accountId: string,
53
+ connected: unknown,
54
+ ): Promise<Response | undefined> {
55
+ if (propertyOf(connected, "reconnected") !== true) return undefined;
56
+ const reactivated = await deps.dispatchSystemWrite({
57
+ handlerQn: InboundMailFoundationHandlers.reactivateAccount,
58
+ tenantId,
59
+ payload: { accountId },
60
+ });
61
+ return reactivated.isSuccess ? undefined : connectFailureResponse(c, reactivated.error);
62
+ }
63
+
64
+ // The callback connects as a system user, so connect-account's role rules (mail member at all,
65
+ // admin for a shared mailbox) have to hold here, before the signed state is handed out.
66
+ function connectRoleRefusal(
67
+ c: Context,
68
+ deps: UserExtraRouteDeps,
69
+ scope: "user" | "shared",
70
+ ): Response | undefined {
71
+ const { memberRoles, adminRoles } = readInboundMailRoles(deps.registry);
72
+ if (!memberRoles.some((role) => deps.user.roles.includes(role))) {
73
+ return errorJson(
74
+ c,
75
+ 403,
76
+ "mail_role_required",
77
+ `${INBOUND_MAIL_FOUNDATION_FEATURE}: connecting a mailbox needs one of the roles ${memberRoles.join(", ")}`,
78
+ );
79
+ }
80
+ if (scope === "shared" && !isMailAdmin(deps.user, adminRoles)) {
81
+ return errorJson(
82
+ c,
83
+ 403,
84
+ "shared_mailbox_requires_admin",
85
+ `${INBOUND_MAIL_FOUNDATION_FEATURE}: only ${adminRoles.join(", ")} connect a shared mailbox`,
86
+ );
87
+ }
88
+ return undefined;
89
+ }
90
+
40
91
  export type InboundMailConnectRoutesOptions = {
41
92
  /** HMAC-secret for the state param (deploy-time env — `scope:"system"`
42
93
  * secrets don't exist in secrets-v1). */
@@ -62,36 +113,6 @@ export type InboundMailConnectRoutesOptions = {
62
113
  readonly metricsFor?: InboundMailContext["metricsFor"];
63
114
  };
64
115
 
65
- function errorJson(c: Context, status: 400 | 401 | 404 | 500 | 502, code: string, message: string) {
66
- return c.json({ error: { code, message } }, status);
67
- }
68
-
69
- type VerifiedCallback = { readonly code: string; readonly state: OAuthStatePayload };
70
-
71
- function verifyCallbackRequest(
72
- request: SignatureExtraRouteVerifyRequest,
73
- stateSecret: string,
74
- ): VerifiedCallback {
75
- const code = request.query["code"] ?? "";
76
- const rawState = request.query["state"] ?? "";
77
- if (!code || !rawState) {
78
- throw new ExtraRouteRejection(
79
- 400,
80
- { error: { code: "invalid_callback", message: "missing code or state" } },
81
- "inbound-mail oauth callback missing code or state",
82
- );
83
- }
84
- const verified = verifyOAuthState(rawState, stateSecret);
85
- if (!verified.ok) {
86
- throw new ExtraRouteRejection(
87
- 400,
88
- { error: { code: "invalid_state", message: `state rejected: ${verified.reason}` } },
89
- `inbound-mail oauth callback state rejected: ${verified.reason}`,
90
- );
91
- }
92
- return { code, state: verified.payload };
93
- }
94
-
95
116
  /** Builds the `entry:"user"`/`entry:"signature"` extraRoute pair. Mount via
96
117
  * `extraRoutes: createInboundMailConnectRoutes(options)`. */
97
118
  export function createInboundMailConnectRoutes(
@@ -101,7 +122,7 @@ export function createInboundMailConnectRoutes(
101
122
 
102
123
  const connectRoute: ExtraRouteDefinition = {
103
124
  method: "GET",
104
- path: options.connectPath ?? DEFAULT_CONNECT_PATH,
125
+ path: options.connectPath ?? INBOUND_MAIL_CONNECT_PATH,
105
126
  entry: "user",
106
127
  handler: async (c: Context, deps: UserExtraRouteDeps): Promise<Response> => {
107
128
  const providerKey = c.req.query("provider") ?? "";
@@ -116,6 +137,9 @@ export function createInboundMailConnectRoutes(
116
137
  );
117
138
  }
118
139
 
140
+ const refused = connectRoleRefusal(c, deps, scope);
141
+ if (refused) return refused;
142
+
119
143
  let plugin: ReturnType<typeof resolveInboundProviderForKey>;
120
144
  try {
121
145
  plugin = resolveInboundProviderForKey({ registry: deps.registry }, providerKey);
@@ -213,17 +237,8 @@ export function createInboundMailConnectRoutes(
213
237
  "token exchange with the provider failed",
214
238
  );
215
239
  }
216
- if (!tokens.refreshToken) {
217
- // No refresh token → no server-side sync possible. Consent must
218
- // request offline_access (or access_type=offline) — the provider's
219
- // oauth block owns the scopes.
220
- return errorJson(
221
- c,
222
- 502,
223
- "no_refresh_token",
224
- `provider "${state.providerKey}" returned no refresh token — check the offline-access scope in the provider's oauth.scopes`,
225
- );
226
- }
240
+ const refreshToken = refreshTokenOrRefusal(c, state, tokens);
241
+ if (typeof refreshToken !== "string") return refreshToken;
227
242
 
228
243
  // Account creation (programmatic SystemUser; the real owner comes from
229
244
  // the HMAC-verified state → ownerUserIdOverride).
@@ -240,30 +255,46 @@ export function createInboundMailConnectRoutes(
240
255
  },
241
256
  });
242
257
  if (!dispatched.isSuccess) {
243
- return errorJson(c, 502, "account_create_failed", JSON.stringify(dispatched.error ?? {}));
258
+ return connectFailureResponse(c, dispatched.error);
244
259
  }
245
260
  const accountId = propertyOf(dispatched.data, "accountId");
246
261
  if (typeof accountId !== "string") {
247
262
  return errorJson(c, 502, "account_create_failed", "connect-account returned no accountId");
248
263
  }
249
264
 
250
- // Refresh-token into the per-account secret slot (Slot = accountId).
251
- // Access tokens (~1h) are never persisted — the supervisor re-mints
252
- // them from the refresh token (oauth-access-token.ts).
253
- if (options.oauthTokens) {
254
- await options.oauthTokens.storeRefreshToken(
265
+ try {
266
+ await storeOAuthCallbackCredential(
267
+ options.oauthTokens,
268
+ deps.secrets,
255
269
  { id: accountId, tenantId: state.tenantId },
256
- tokens.refreshToken,
270
+ tokens,
271
+ refreshToken,
257
272
  );
258
- options.oauthTokens.primeFromExchange({ id: accountId }, tokens);
259
- } else {
260
- await deps.secrets.set(
261
- state.tenantId,
262
- inboundCredentialSecretKey(accountId),
263
- tokens.refreshToken,
264
- inboundRefreshTokenSecretOptions(accountId),
273
+ } catch {
274
+ return errorJson(
275
+ c,
276
+ 502,
277
+ "credential_store_failed",
278
+ "the sign-in worked but its token could not be saved; the mailbox keeps its previous state, connect it again",
265
279
  );
266
280
  }
281
+ const refused = await reactivateIfReopened(
282
+ c,
283
+ deps,
284
+ state.tenantId,
285
+ accountId,
286
+ dispatched.data,
287
+ );
288
+ if (refused) return refused;
289
+
290
+ // Best effort: the account commit already woke the supervisor before the token existed.
291
+ // Without this event the first sync would wait for the next poll tick; a failure here
292
+ // only costs that delay, so the connect itself still succeeds.
293
+ await deps.dispatchSystemWrite({
294
+ handlerQn: InboundMailFoundationHandlers.markCredentialStored,
295
+ tenantId: state.tenantId,
296
+ payload: { accountId },
297
+ });
267
298
 
268
299
  if (options.successRedirectUrl) {
269
300
  const target = new URL(options.successRedirectUrl);
@@ -1,5 +1,10 @@
1
- // Feature name
2
- export const INBOUND_MAIL_FOUNDATION_FEATURE = "inbound-mail-foundation" as const;
1
+ import { INBOUND_MAIL_FOUNDATION_FEATURE } from "./web/constants.ts";
2
+
3
+ export {
4
+ INBOUND_MAIL_CONNECT_PATH,
5
+ INBOUND_MAIL_FOUNDATION_FEATURE,
6
+ INBOUND_MAIL_RECONNECT_OAUTH_ACTION,
7
+ } from "./web/constants.ts";
3
8
 
4
9
  // Extension-point name for provider plugins (inbound-provider-imap,
5
10
  // inbound-provider-m365-graph, inbound-provider-gmail-rest, ...).
@@ -20,6 +25,18 @@ export const InboundMailFoundationHandlers = {
20
25
  /** Tenant admin disconnects a mailbox. The stream remains for auditing;
21
26
  * the status becomes disconnected and the watch stops. */
22
27
  disconnectAccount: "inbound-mail-foundation:write:disconnect-account",
28
+ /** The only way from disconnected back to active; also clears auth_error
29
+ * and degraded. update-account keeps rejecting disconnected accounts. */
30
+ reactivateAccount: "inbound-mail-foundation:write:reactivate-account",
31
+ setTrustedAuthservId: "inbound-mail-foundation:write:set-trusted-authserv-id",
32
+ /** Owner or admin asks for one quarantined mail to be loaded again on the next sync. */
33
+ retryQuarantinedMessage: "inbound-mail-foundation:write:retry-quarantined-message",
34
+ /** Marks all open quarantine entries of a mailbox as acknowledged. */
35
+ acknowledgeQuarantine: "inbound-mail-foundation:write:acknowledge-quarantine",
36
+ /** Supervisor-only: stores the content of an already imported snippet mail. */
37
+ attachMessageContent: "inbound-mail-foundation:write:attach-message-content",
38
+ /** OAuth callback only: the refresh token is stored now, so an active account can sync. */
39
+ markCredentialStored: "inbound-mail-foundation:write:mark-credential-stored",
23
40
  } as const;
24
41
 
25
42
  // Qualified query handler names.
@@ -29,6 +46,10 @@ export const InboundMailFoundationQueries = {
29
46
  /** Tenant-scoped message list (inbox cockpit). PII is decrypted on read.
30
47
  */
31
48
  listMessages: "inbound-mail-foundation:query:message:list",
49
+ /** Open quarantine entries of the mailboxes the caller may manage, with sender and subject from the header import. */
50
+ listQuarantine: "inbound-mail-foundation:query:quarantine:list",
51
+ /** Decrypted full text, attachment ids and content state of one message. */
52
+ messageContent: "inbound-mail-foundation:query:message:content",
32
53
  } as const;
33
54
 
34
55
  // Normalized account status values — provider-agnostic.
@@ -37,7 +58,7 @@ export const InboundMailAccountStatuses = {
37
58
  active: "active",
38
59
  /** Credentials or token are invalid; the tenant admin must reconnect. */
39
60
  authError: "auth_error",
40
- /** Transienter Fehler, Supervisor backoff-retried. */
61
+ /** Interrupted: transient failure, quarantined mail, missing file storage or KMS; see lastError. */
41
62
  degraded: "degraded",
42
63
  /** Disconnected by the tenant admin; no further syncs run. */
43
64
  disconnected: "disconnected",
@@ -45,6 +66,34 @@ export const InboundMailAccountStatuses = {
45
66
  export type InboundMailAccountStatus =
46
67
  (typeof InboundMailAccountStatuses)[keyof typeof InboundMailAccountStatuses];
47
68
 
69
+ // Why a mailbox is degraded. Codes only, never provider error text (that carries hosts/tokens).
70
+ export const InboundMailLastErrors = {
71
+ connectionFailed: "connection_failed",
72
+ rateLimited: "rate_limited",
73
+ messageQuarantined: "message_quarantined",
74
+ fileStorageUnavailable: "file_storage_unavailable",
75
+ encryptionUnavailable: "encryption_unavailable",
76
+ ingestUnavailable: "ingest_unavailable",
77
+ } as const;
78
+ export type InboundMailLastError =
79
+ (typeof InboundMailLastErrors)[keyof typeof InboundMailLastErrors];
80
+
81
+ // Business reasons of failed connect writes; connect-routes maps both to 409 with the reason code.
82
+ export const InboundMailConnectErrors = {
83
+ addressAlreadyConnected: {
84
+ reason: "address_already_connected",
85
+ i18nKey: "inboundMail.accounts.errors.addressAlreadyConnected",
86
+ },
87
+ addressDisconnected: {
88
+ reason: "address_disconnected",
89
+ i18nKey: "inboundMail.accounts.errors.addressDisconnected",
90
+ },
91
+ credentialRequired: {
92
+ reason: "credential_required",
93
+ i18nKey: "inboundMail.accounts.errors.credentialRequired",
94
+ },
95
+ } as const;
96
+
48
97
  /**
49
98
  * Tenant secret slot for an account: IMAP app password OR OAuth refresh
50
99
  * token. The provider interprets the contents according to `authMethod`.
@@ -71,6 +120,20 @@ export const InboundMailAuthMethods = {
71
120
  export type InboundMailAuthMethod =
72
121
  (typeof InboundMailAuthMethods)[keyof typeof InboundMailAuthMethods];
73
122
 
123
+ /** Provider key of inbound-provider-imap: only its password mailboxes have a credential form. */
124
+ export const IMAP_PROVIDER_KEY = "imap";
125
+
126
+ // How a mailbox the caller may re-arm gets its credential back, computed by account:list because a
127
+ // screen row condition cannot combine provider, auth method, status and owner.
128
+ export const MailAccountReactivationPaths = {
129
+ /** The IMAP form of inbound-provider-imap (reconnect-mailbox). */
130
+ passwordForm: "password_form",
131
+ /** The provider's OAuth sign-in through the connect route; its callback reopens the account. */
132
+ oauthFlow: "oauth_flow",
133
+ } as const;
134
+ export type MailAccountReactivationPath =
135
+ (typeof MailAccountReactivationPaths)[keyof typeof MailAccountReactivationPaths];
136
+
74
137
  // **Multi-provider:** no global `provider` config key
75
138
  // (unlike mail-foundation, as with billing-foundation). A tenant can connect
76
139
  // an IMAP mailbox and an M365 mailbox in parallel; the provider is stored per
@@ -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,