@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,129 @@
1
+ // reactivate-account — the only way from disconnected back to active (it also clears auth_error
2
+ // and degraded). update-account keeps rejecting disconnected accounts so a late supervisor tick
3
+ // can never undo a disconnect; a deliberate reconnect goes through here instead, keeping the
4
+ // accountId and with it cursor, seen anchors and threads.
5
+
6
+ import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
7
+ import {
8
+ type HandlerContext,
9
+ SYSTEM_ROLE,
10
+ SYSTEM_USER_ID,
11
+ type WriteHandlerDef,
12
+ } from "@cosmicdrift/kumiko-framework/engine";
13
+ import {
14
+ AccessDeniedError,
15
+ failNotFound,
16
+ UnprocessableError,
17
+ writeFailure,
18
+ } from "@cosmicdrift/kumiko-framework/errors";
19
+ import * as z from "zod";
20
+ import {
21
+ InboundMailAccountStatuses,
22
+ InboundMailConnectErrors,
23
+ InboundMailFoundationHandlers,
24
+ inboundCredentialSecretKey,
25
+ } from "../constants.ts";
26
+ import {
27
+ MAIL_ACCOUNT_AGGREGATE_TYPE,
28
+ MAIL_ACCOUNT_UPDATED_EVENT_QN,
29
+ type MailAccountEventHeaders,
30
+ type MailAccountEventPayload,
31
+ } from "../events.ts";
32
+ import { accessRolesOf, type InboundMailRoles } from "../mail-roles.ts";
33
+ import { loadCurrentMailAccountPayload } from "./account-state.ts";
34
+ import { mayReactivateMailbox } from "./scope-visibility.ts";
35
+
36
+ export const RECONNECT_AUDIT_REASON = "reconnect";
37
+
38
+ // Strict: the trusted authserv-id only changes through set-trusted-authserv-id.
39
+ export const reactivateAccountSchema = z.object({ accountId: z.uuid() }).strict();
40
+
41
+ const CREDENTIAL_PRESENCE_REASON =
42
+ "the credential secret is checked on the request transaction because reconnect-mailbox writes it earlier in the same transaction, which a separate connection would not see";
43
+
44
+ // The OAuth callback re-arms a personal mailbox for its owner after the owner signed in: the signed
45
+ // state carries the owner, the dispatcher only sees the system user.
46
+ function isSystemActor(user: { readonly id: string; readonly roles: readonly string[] }): boolean {
47
+ return user.id === SYSTEM_USER_ID && user.roles.includes(SYSTEM_ROLE);
48
+ }
49
+
50
+ function isAlreadyHealthy(current: MailAccountEventPayload): boolean {
51
+ return (
52
+ current.status === InboundMailAccountStatuses.active &&
53
+ current.watchState === "idle" &&
54
+ current.lastError === null
55
+ );
56
+ }
57
+
58
+ // Returns false when nothing had to change.
59
+ async function appendMailAccountReactivation(
60
+ ctx: Pick<HandlerContext, "events">,
61
+ accountId: string,
62
+ current: MailAccountEventPayload,
63
+ ): Promise<boolean> {
64
+ if (isAlreadyHealthy(current)) return false;
65
+ const eventPayload: MailAccountEventPayload = {
66
+ ...current,
67
+ status: InboundMailAccountStatuses.active,
68
+ watchState: "idle",
69
+ lastError: null,
70
+ };
71
+ const headers: MailAccountEventHeaders = {
72
+ providerName: current.provider,
73
+ reason: RECONNECT_AUDIT_REASON,
74
+ };
75
+ await ctx.events.unsafeAppend({
76
+ aggregateId: accountId,
77
+ aggregateType: MAIL_ACCOUNT_AGGREGATE_TYPE,
78
+ type: MAIL_ACCOUNT_UPDATED_EVENT_QN,
79
+ payload: eventPayload,
80
+ headers,
81
+ });
82
+ return true;
83
+ }
84
+
85
+ export function createReactivateAccountHandler({
86
+ memberRoles,
87
+ adminRoles,
88
+ }: InboundMailRoles): WriteHandlerDef {
89
+ return {
90
+ name: "reactivate-account",
91
+ schema: reactivateAccountSchema,
92
+ // Members: the owner re-arms a personal mailbox; mayReactivateMailbox keeps shared ones to admins.
93
+ access: { roles: accessRolesOf(InboundMailFoundationHandlers.reactivateAccount, memberRoles) },
94
+ agent: { expose: false },
95
+ escapeHatch: { grants: ["unsafeRaw"], reason: CREDENTIAL_PRESENCE_REASON },
96
+ description:
97
+ "Brings a disconnected, interrupted or sign-in-failed mailbox back to active under its existing accountId, so cursor, seen messages and threads survive; the caller replaces the credential in the same flow.",
98
+ handler: async (event, ctx) => {
99
+ const payload = reactivateAccountSchema.parse(event.payload);
100
+ const current = await loadCurrentMailAccountPayload(ctx, payload.accountId);
101
+ if (!current) return failNotFound("mail-account", payload.accountId);
102
+ if (
103
+ !isSystemActor(event.user) &&
104
+ !mayReactivateMailbox(current.ownerUserId, event.user, adminRoles)
105
+ ) {
106
+ return writeFailure(new AccessDeniedError({ details: { reason: "not_mailbox_owner" } }));
107
+ }
108
+ // Nothing to re-arm, so no credential is needed either.
109
+ if (isAlreadyHealthy(current)) {
110
+ return { isSuccess: true as const, data: { accountId: payload.accountId, changed: false } };
111
+ }
112
+ // Every other starting state: without a credential the supervisor would only fail again.
113
+ const secrets = requireSecretsContext(
114
+ { secrets: ctx.services.secrets, user: ctx.user },
115
+ "reactivate-account",
116
+ ).withRunner(ctx.db.unsafeRaw());
117
+ const hasCredential = await secrets.has(
118
+ event.user.tenantId,
119
+ inboundCredentialSecretKey(payload.accountId),
120
+ );
121
+ if (!hasCredential) {
122
+ const { reason, i18nKey } = InboundMailConnectErrors.credentialRequired;
123
+ return writeFailure(new UnprocessableError(reason, { i18nKey }));
124
+ }
125
+ const changed = await appendMailAccountReactivation(ctx, payload.accountId, current);
126
+ return { isSuccess: true as const, data: { accountId: payload.accountId, changed } };
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,49 @@
1
+ // retry-quarantined-message — marks one quarantined mail for a reload. The next successful sync of
2
+ // the mailbox fetches it again and, when it now goes through, removes the quarantine row
3
+ // (quarantine-retry.ts). The handler only flags; the supervisor owns the provider connection.
4
+
5
+ import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
6
+ import {
7
+ AccessDeniedError,
8
+ failNotFound,
9
+ writeFailure,
10
+ } from "@cosmicdrift/kumiko-framework/errors";
11
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
12
+ import * as z from "zod";
13
+ import type { InboundMailRoles } from "../mail-roles.ts";
14
+ import { quarantineTable } from "../quarantine-entities.ts";
15
+ import { loadCurrentMailAccountPayload } from "./account-state.ts";
16
+ import { mayManageQuarantine } from "./scope-visibility.ts";
17
+
18
+ export const retryQuarantinedMessageSchema = z
19
+ .object({ accountId: z.uuid(), providerMessageId: z.string().min(1).max(500) })
20
+ .strict();
21
+
22
+ export function createRetryQuarantinedMessageHandler({
23
+ memberRoles,
24
+ adminRoles,
25
+ }: InboundMailRoles): WriteHandlerDef {
26
+ return {
27
+ name: "retry-quarantined-message",
28
+ schema: retryQuarantinedMessageSchema,
29
+ access: { roles: [...memberRoles] },
30
+ agent: { expose: false },
31
+ description:
32
+ "Asks for a quarantined mail to be loaded again from the provider on the next sync of its mailbox; the owner of a personal mailbox or, for a shared one, an admin may call it, and the quarantine entry disappears once the mail went through.",
33
+ handler: async (event, ctx) => {
34
+ const payload = retryQuarantinedMessageSchema.parse(event.payload);
35
+ const account = await loadCurrentMailAccountPayload(ctx, payload.accountId);
36
+ if (!account) return failNotFound("mail-account", payload.accountId);
37
+ if (!mayManageQuarantine(account.ownerUserId, event.user, adminRoles)) {
38
+ return writeFailure(new AccessDeniedError({ details: { reason: "not_mailbox_owner" } }));
39
+ }
40
+ const updated = await ctx.db.updateMany(
41
+ quarantineTable,
42
+ { retryRequestedAt: Temporal.Now.instant().toString() },
43
+ { accountId: payload.accountId, providerMessageId: payload.providerMessageId },
44
+ );
45
+ if (updated.length === 0) return failNotFound("mail-quarantine", payload.providerMessageId);
46
+ return { isSuccess: true as const, data: { requested: true as const } };
47
+ },
48
+ };
49
+ }
@@ -1,13 +1,52 @@
1
- // Scope visibility: shared rows (`ownerUserId=null`) are visible to any authorized caller;
2
- // personal rows are visible only to the owner, `TenantAdmin`, or `SystemAdmin` for compliance.
3
- // The same rule applies to `read_mail_accounts` and `read_inbound_messages`.
1
+ // Scope visibility: shared rows (`ownerUserId=null`) are visible to any authorized caller.
2
+ // A personal mailbox is private to its owner: its messages, bodies, attachments and quarantine are
3
+ // never shown to admins, who see only the operational metadata of the account (address, status).
4
4
 
5
- export function isVisibleToCaller(
5
+ type Caller = { readonly id: string; readonly roles: readonly string[] };
6
+
7
+ function isShared(ownerUserId: unknown): boolean {
8
+ return ownerUserId == null || ownerUserId === "";
9
+ }
10
+
11
+ export function isVisibleToCaller(row: Readonly<Record<string, unknown>>, user: Caller): boolean {
12
+ const owner = row["ownerUserId"];
13
+ return isShared(owner) || owner === user.id;
14
+ }
15
+
16
+ export function isMailAdmin(
17
+ user: { readonly roles: readonly string[] },
18
+ adminRoles: readonly string[],
19
+ ): boolean {
20
+ return adminRoles.some((role) => user.roles.includes(role));
21
+ }
22
+
23
+ // Account metadata (not content) of a personal mailbox stays visible to admins for operations.
24
+ export function isAccountListableBy(
6
25
  row: Readonly<Record<string, unknown>>,
7
- user: { readonly id: string; readonly roles: readonly string[] },
26
+ user: Caller,
27
+ adminRoles: readonly string[],
8
28
  ): boolean {
9
- const owner = row["ownerUserId"];
10
- if (owner == null || owner === "") return true;
11
- if (owner === user.id) return true;
12
- return user.roles.includes("TenantAdmin") || user.roles.includes("SystemAdmin");
29
+ return isVisibleToCaller(row, user) || isMailAdmin(user, adminRoles);
30
+ }
31
+
32
+ // A personal mailbox is re-armed (and its sender check changed) by its owner only: a reconnect
33
+ // re-reads the owner's mail with whatever credential the caller supplies, so an admin must not do
34
+ // it. A shared mailbox belongs to the tenant: admins only.
35
+ export function mayReactivateMailbox(
36
+ ownerUserId: unknown,
37
+ user: Caller,
38
+ adminRoles: readonly string[],
39
+ ): boolean {
40
+ if (isShared(ownerUserId)) return isMailAdmin(user, adminRoles);
41
+ return ownerUserId === user.id;
42
+ }
43
+
44
+ // Quarantine entries carry sender and subject of the mail, so those of a personal mailbox are the
45
+ // owner's alone; shared mailboxes are administered by admins.
46
+ export function mayManageQuarantine(
47
+ ownerUserId: string | null,
48
+ user: Caller,
49
+ adminRoles: readonly string[],
50
+ ): boolean {
51
+ return isShared(ownerUserId) ? isMailAdmin(user, adminRoles) : ownerUserId === user.id;
13
52
  }
@@ -0,0 +1,66 @@
1
+ // set-trusted-authserv-id — the one place that changes which Authentication-Results header the
2
+ // sender check believes. It is separate from connect/update/reactivate on purpose: a payload that
3
+ // can smuggle this value in through a generic update would let any caller mark forged headers as
4
+ // trusted, and it must never be a model-callable tool.
5
+
6
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
8
+ import * as z from "zod";
9
+ import { InboundMailAccountStatuses } from "../constants.ts";
10
+ import {
11
+ MAIL_ACCOUNT_AGGREGATE_TYPE,
12
+ MAIL_ACCOUNT_UPDATED_EVENT_QN,
13
+ type MailAccountEventHeaders,
14
+ type MailAccountEventPayload,
15
+ } from "../events.ts";
16
+ import type { InboundMailRoles } from "../mail-roles.ts";
17
+ import { normalizeTrustedAuthservId } from "../sender-auth.ts";
18
+ import { loadCurrentMailAccountPayload } from "./account-state.ts";
19
+ import { trustedAuthservIdInputSchema } from "./trusted-authserv-id.ts";
20
+ import { auditReasonFor } from "./update-account.write.ts";
21
+
22
+ export const setTrustedAuthservIdSchema = z
23
+ .object({
24
+ accountId: z.uuid(),
25
+ trustedAuthservId: trustedAuthservIdInputSchema,
26
+ })
27
+ .strict();
28
+
29
+ export const createSetTrustedAuthservIdHandler = ({ adminRoles }: InboundMailRoles) =>
30
+ defineWriteHandler({
31
+ name: "set-trusted-authserv-id",
32
+ schema: setTrustedAuthservIdSchema,
33
+ // A tenant security policy, personal mailboxes included: a trusted authserv-id makes forged senders
34
+ // count as authenticated for the whole tenant (contact matching, auto replies), so owners never set it.
35
+ access: { roles: [...adminRoles] },
36
+ agent: { expose: false },
37
+ description:
38
+ "Sets or clears the authserv-id of the mailbox's own server, which decides whose Authentication-Results header the sender check trusts; null or empty clears it, a connected mailbox only.",
39
+ handler: async (event, ctx) => {
40
+ const payload = event.payload;
41
+ const current = await loadCurrentMailAccountPayload(ctx, payload.accountId);
42
+ if (!current) return failNotFound("mail-account", payload.accountId);
43
+ if (current.status === InboundMailAccountStatuses.disconnected) {
44
+ return failUnprocessable("account_disconnected", { accountId: payload.accountId });
45
+ }
46
+
47
+ const trustedAuthservId = normalizeTrustedAuthservId(payload.trustedAuthservId);
48
+ if (current.trustedAuthservId === trustedAuthservId) {
49
+ return { isSuccess: true as const, data: { accountId: payload.accountId, changed: false } };
50
+ }
51
+
52
+ const eventPayload: MailAccountEventPayload = { ...current, trustedAuthservId };
53
+ const headers: MailAccountEventHeaders = {
54
+ providerName: current.provider,
55
+ reason: auditReasonFor(event.user, undefined),
56
+ };
57
+ await ctx.events.unsafeAppend({
58
+ aggregateId: payload.accountId,
59
+ aggregateType: MAIL_ACCOUNT_AGGREGATE_TYPE,
60
+ type: MAIL_ACCOUNT_UPDATED_EVENT_QN,
61
+ payload: eventPayload,
62
+ headers,
63
+ });
64
+ return { isSuccess: true as const, data: { accountId: payload.accountId, changed: true } };
65
+ },
66
+ });
@@ -0,0 +1,136 @@
1
+ import {
2
+ type configuredPiiSubjectKms,
3
+ encryptPiiFieldValues,
4
+ } from "@cosmicdrift/kumiko-framework/crypto";
5
+ import { acquireNamespacedAdvisoryLock, countWhere } from "@cosmicdrift/kumiko-framework/db";
6
+ import type { HandlerContext, TenantId } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
8
+ import { MAIL_THREAD_PII_FIELDS, mailThreadEntity } from "../entities.ts";
9
+ import {
10
+ type InboundMessageEventHeaders,
11
+ MAIL_THREAD_AGGREGATE_TYPE,
12
+ MAIL_THREAD_UPDATED_EVENT_QN,
13
+ type MailThreadEventPayload,
14
+ } from "../events.ts";
15
+ import { inboundMessagesProjectionTable } from "../projection.ts";
16
+ import type { IngestMessagePayload } from "./ingest-message-schema.ts";
17
+
18
+ const THREAD_ROLLUP_MAX_ATTEMPTS = 5;
19
+ // pg_advisory_xact_lock namespace (int4) for the step-5 thread-rollup lock
20
+ // — 'inbm' as ASCII, keeps it disjoint from the framework's other fixed
21
+ // single-int advisory-lock keys (schema bootstrap, es-ops boot).
22
+ const THREAD_ROLLUP_LOCK_NAMESPACE = 0x696e626d;
23
+
24
+ /**
25
+ * Normalizes threadKey:
26
+ * 1. Use the provider thread ID when present — prefix it with the provider
27
+ * so IMAP References keys and Graph conversationIds never collide.
28
+ * 2. Otherwise use the root of the References chain (the oldest Message-ID,
29
+ * which is the thread anchor shared by all replies).
30
+ * 3. Otherwise use the own Message-ID → single-message thread; a later
31
+ * reply carries it as the References root and joins the same thread.
32
+ */
33
+ export function buildThreadKey(p: IngestMessagePayload, effectiveMessageIdHeader: string): string {
34
+ if (p.providerThreadId) return `${p.providerName}:${p.providerThreadId}`.slice(0, 500);
35
+ const referencesRoot = p.references[0];
36
+ if (referencesRoot) return `mid:${referencesRoot}`.slice(0, 500);
37
+ return `mid:${effectiveMessageIdHeader}`.slice(0, 500);
38
+ }
39
+
40
+ export type ThreadRollupInput = {
41
+ readonly tenantId: TenantId;
42
+ readonly threadAggId: string;
43
+ readonly threadKey: string;
44
+ readonly subject: string;
45
+ readonly receivedAtIso: string;
46
+ readonly headers: InboundMessageEventHeaders;
47
+ readonly piiKms: ReturnType<typeof configuredPiiSubjectKms>;
48
+ };
49
+
50
+ // ---------------------------------------------------------------
51
+ // 5. Thread-Rollup. New snapshot (live COUNT + max(lastMessageAt)) via
52
+ // ctx.events.tryAppend in a bounded retry loop — the apply stays a
53
+ // dumb, replay-idempotent UPSERT. lastMessageAtIso is not PII (a
54
+ // timestamp), so it stays plaintext-comparable; subject is
55
+ // encrypted once up front (constant across retries, doesn't
56
+ // depend on the thread's prior state).
57
+ //
58
+ // messageCount is a live COUNT, not previousCount+1, so two
59
+ // concurrent ingests on the same thread (Watch-Push vs.
60
+ // Poll-Reconciliation overlap, or two different messages of the
61
+ // same thread arriving at once) converge on the true count.
62
+ // acquireNamespacedAdvisoryLock below serializes this whole
63
+ // countWhere+append section per threadAggId — transaction-scoped,
64
+ // so it survives events.tryAppend's inner SAVEPOINT and releases at
65
+ // the enclosing TX's commit/rollback. Without it, a concurrent
66
+ // commit landing between countWhere and events.tryAppend's own fresh
67
+ // version-read makes the append succeed with a stale count instead
68
+ // of a VersionConflictError, so the retry loop below never sees it
69
+ // as a conflict to retry (the original gap). Unlike step 4, a
70
+ // lost version-conflict race here must NOT be treated as a no-op
71
+ // duplicate — skipping would leave this message's contribution to
72
+ // messageCount/lastMessageAt out of the thread forever. Exhausting
73
+ // all attempts throws, rolling back the whole TX (including the
74
+ // step-4 message append) for a clean re-ingest.
75
+ // ---------------------------------------------------------------
76
+ export async function appendThreadRollup(
77
+ ctx: HandlerContext,
78
+ input: ThreadRollupInput,
79
+ ): Promise<void> {
80
+ const { tenantId, threadAggId, threadKey, subject, receivedAtIso, headers, piiKms } = input;
81
+ const threadPlainPii = { tenantId, subject: subject };
82
+ const encryptedThreadFields = piiKms
83
+ ? await encryptPiiFieldValues(
84
+ threadPlainPii,
85
+ mailThreadEntity,
86
+ MAIL_THREAD_PII_FIELDS,
87
+ piiKms,
88
+ {
89
+ requestId: `inbound-mail-foundation:ingest-message:thread:${threadAggId}`,
90
+ tenantId,
91
+ },
92
+ { tenantId, entityName: MAIL_THREAD_AGGREGATE_TYPE },
93
+ )
94
+ : threadPlainPii;
95
+
96
+ const threadRollupRunner = ctx.db.unsafeRaw();
97
+ await acquireNamespacedAdvisoryLock(
98
+ threadRollupRunner,
99
+ THREAD_ROLLUP_LOCK_NAMESPACE,
100
+ threadAggId,
101
+ );
102
+
103
+ let threadAppendOk = false;
104
+ for (let attempt = 1; attempt <= THREAD_ROLLUP_MAX_ATTEMPTS && !threadAppendOk; attempt++) {
105
+ const threadEvents = await ctx.events.load(threadAggId);
106
+ const lastThreadEvent = threadEvents[threadEvents.length - 1];
107
+ // @cast-boundary engine-payload — eigene events, shape via defineEvent
108
+ const previousThread = lastThreadEvent?.payload as MailThreadEventPayload | undefined;
109
+ const previousLastAt = previousThread?.lastMessageAtIso ?? "";
110
+ const newLastAt = receivedAtIso > previousLastAt ? receivedAtIso : previousLastAt;
111
+ const messageCount = await countWhere(threadRollupRunner, inboundMessagesProjectionTable, {
112
+ tenantId,
113
+ threadKey,
114
+ });
115
+
116
+ const threadEventPayload: MailThreadEventPayload = {
117
+ threadKey,
118
+ subject: encryptedThreadFields.subject,
119
+ lastMessageAtIso: newLastAt,
120
+ messageCount,
121
+ };
122
+ const threadAppend = await ctx.events.tryAppend({
123
+ aggregateId: threadAggId,
124
+ aggregateType: MAIL_THREAD_AGGREGATE_TYPE,
125
+ type: MAIL_THREAD_UPDATED_EVENT_QN,
126
+ payload: threadEventPayload,
127
+ headers,
128
+ });
129
+ threadAppendOk = threadAppend.ok;
130
+ }
131
+ if (!threadAppendOk) {
132
+ throw new InternalError({
133
+ message: `ingest-message: thread-rollup append for "${threadAggId}" lost the version-conflict race ${THREAD_ROLLUP_MAX_ATTEMPTS} times in a row — aborting for a clean re-ingest instead of persisting a stale messageCount.`,
134
+ });
135
+ }
136
+ }
@@ -0,0 +1,4 @@
1
+ import * as z from "zod";
2
+
3
+ /** authserv-id of the mailbox's own server, set only through set-trusted-authserv-id; null or empty clears it. */
4
+ export const trustedAuthservIdInputSchema = z.string().trim().max(255).nullable();
@@ -11,82 +11,104 @@ import {
11
11
  } from "@cosmicdrift/kumiko-framework/engine";
12
12
  import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
13
13
  import * as z from "zod";
14
- import { InboundMailAccountStatuses } from "../constants.ts";
14
+ import {
15
+ InboundMailAccountStatuses,
16
+ InboundMailFoundationHandlers,
17
+ InboundMailLastErrors,
18
+ } from "../constants.ts";
15
19
  import {
16
20
  MAIL_ACCOUNT_AGGREGATE_TYPE,
17
21
  MAIL_ACCOUNT_UPDATED_EVENT_QN,
18
22
  type MailAccountEventHeaders,
19
23
  type MailAccountEventPayload,
20
24
  } from "../events.ts";
25
+ import { accessRolesOf, type InboundMailRoles } from "../mail-roles.ts";
21
26
  import { loadCurrentMailAccountPayload } from "./account-state.ts";
22
27
 
23
- export const updateAccountSchema = z.object({
24
- accountId: z.uuid(),
25
- status: z
26
- .enum([
27
- InboundMailAccountStatuses.active,
28
- InboundMailAccountStatuses.authError,
29
- InboundMailAccountStatuses.degraded,
30
- ])
31
- .optional(),
32
- /** Ops-Signal ("idle", "watching", "backoff:3", ...). */
33
- watchState: z.string().max(100).optional(),
34
- displayName: z.string().max(200).optional(),
35
- /** Audit reason for metadata.headers, honored for the system identity only
36
- * ("watch_supervisor", "oauth_refresh"); user callers get a role-derived reason. */
37
- reason: z.string().min(1).max(100).optional(),
38
- });
28
+ export const updateAccountSchema = z
29
+ .object({
30
+ accountId: z.uuid(),
31
+ status: z
32
+ .enum([
33
+ InboundMailAccountStatuses.active,
34
+ InboundMailAccountStatuses.authError,
35
+ InboundMailAccountStatuses.degraded,
36
+ ])
37
+ .optional(),
38
+ /** Why the mailbox is degraded; null clears it. Codes only, never provider error text. */
39
+ lastError: z.enum(Object.values(InboundMailLastErrors)).nullable().optional(),
40
+ /** Ops-Signal ("idle", "watching", "backoff:3", ...). */
41
+ watchState: z.string().max(100).optional(),
42
+ displayName: z.string().trim().max(200).optional(),
43
+ /** Audit reason for metadata.headers, honored for the system identity only
44
+ * ("watch_supervisor", "oauth_refresh"); user callers get a role-derived reason. */
45
+ reason: z.string().min(1).max(100).optional(),
46
+ })
47
+ .strict();
39
48
  type UpdateAccountPayload = z.infer<typeof updateAccountSchema>;
40
49
 
41
50
  const SYSTEM_FALLBACK_REASON = "system";
42
51
 
43
- function auditReasonFor(user: SessionUser, requestedReason: string | undefined): string {
52
+ export function auditReasonFor(user: SessionUser, requestedReason: string | undefined): string {
44
53
  if (user.id === SYSTEM_USER_ID) return requestedReason ?? SYSTEM_FALLBACK_REASON;
45
54
  return user.roles.includes(ROLES.SystemAdmin) ? "system_admin" : "tenant_admin";
46
55
  }
47
56
 
48
- export const updateAccountHandler: WriteHandlerDef = {
49
- name: "update-account",
50
- schema: updateAccountSchema,
51
- access: { roles: ["SystemAdmin", "TenantAdmin"] },
52
- description:
53
- "Records a status, watch-state or display-name change on a connected mailbox as a new snapshot with an audit reason, rejecting any change to an already disconnected mailbox; use it to flag an auth error or to re-activate a mailbox after fixing its credentials.",
54
- handler: async (event, ctx) => {
55
- // @cast-boundary engine-payload — dispatcher-zod-validated payload
56
- const payload = event.payload as UpdateAccountPayload;
57
+ export function createUpdateAccountHandler({ adminRoles }: InboundMailRoles): WriteHandlerDef {
58
+ return {
59
+ name: "update-account",
60
+ schema: updateAccountSchema,
61
+ access: { roles: accessRolesOf(InboundMailFoundationHandlers.updateAccount, adminRoles) },
62
+ description:
63
+ "Renames a connected mailbox (display name) as a new snapshot with an audit reason, rejecting any change to an already disconnected mailbox; status, error and watch state are reserved for the system identity.",
64
+ handler: async (event, ctx) => {
65
+ // @cast-boundary engine-payload — dispatcher-zod-validated payload
66
+ const payload = event.payload as UpdateAccountPayload;
67
+
68
+ // Status, error and watch state are machine signals of the supervisor and the OAuth refresh; an admin
69
+ // setting them could hide an auth error or fake an active mailbox.
70
+ const isSystemOnlyField =
71
+ payload.status !== undefined ||
72
+ payload.lastError !== undefined ||
73
+ payload.watchState !== undefined ||
74
+ payload.reason !== undefined;
75
+ if (isSystemOnlyField && event.user.id !== SYSTEM_USER_ID) {
76
+ return failUnprocessable("field_requires_system_identity");
77
+ }
57
78
 
58
- const current = await loadCurrentMailAccountPayload(ctx, payload.accountId);
59
- if (!current) {
60
- return failNotFound("mail-account", payload.accountId);
61
- }
62
- if (current.status === InboundMailAccountStatuses.disconnected) {
63
- // Disconnected accounts are final; re-enabling requires a fresh connect
64
- // (new stream, new secret slot). This prevents a delayed supervisor tick
65
- // from restoring a disconnected account to active (disconnect vs. poll
66
- // race).
67
- return failUnprocessable("account_disconnected", { accountId: payload.accountId });
68
- }
79
+ const current = await loadCurrentMailAccountPayload(ctx, payload.accountId);
80
+ if (!current) {
81
+ return failNotFound("mail-account", payload.accountId);
82
+ }
83
+ if (current.status === InboundMailAccountStatuses.disconnected) {
84
+ // Only reactivate-account may leave disconnected. This prevents a delayed
85
+ // supervisor tick from restoring a disconnected account to active
86
+ // (disconnect vs. poll race).
87
+ return failUnprocessable("account_disconnected", { accountId: payload.accountId });
88
+ }
69
89
 
70
- // address/provider/authMethod come unchanged from the latest snapshot
71
- // (address remains ciphertext—never re-encrypt it).
72
- const eventPayload: MailAccountEventPayload = {
73
- ...current,
74
- ...(payload.status !== undefined ? { status: payload.status } : {}),
75
- ...(payload.watchState !== undefined ? { watchState: payload.watchState } : {}),
76
- ...(payload.displayName !== undefined ? { displayName: payload.displayName } : {}),
77
- };
78
- const headers: MailAccountEventHeaders = {
79
- providerName: current.provider,
80
- reason: auditReasonFor(event.user, payload.reason),
81
- };
82
- await ctx.events.unsafeAppend({
83
- aggregateId: payload.accountId,
84
- aggregateType: MAIL_ACCOUNT_AGGREGATE_TYPE,
85
- type: MAIL_ACCOUNT_UPDATED_EVENT_QN,
86
- payload: eventPayload,
87
- headers,
88
- });
90
+ // address/provider/authMethod come unchanged from the latest snapshot
91
+ // (address remains ciphertext—never re-encrypt it).
92
+ const eventPayload: MailAccountEventPayload = {
93
+ ...current,
94
+ ...(payload.status !== undefined ? { status: payload.status } : {}),
95
+ ...(payload.watchState !== undefined ? { watchState: payload.watchState } : {}),
96
+ ...(payload.displayName !== undefined ? { displayName: payload.displayName } : {}),
97
+ ...(payload.lastError !== undefined ? { lastError: payload.lastError } : {}),
98
+ };
99
+ const headers: MailAccountEventHeaders = {
100
+ providerName: current.provider,
101
+ reason: auditReasonFor(event.user, payload.reason),
102
+ };
103
+ await ctx.events.unsafeAppend({
104
+ aggregateId: payload.accountId,
105
+ aggregateType: MAIL_ACCOUNT_AGGREGATE_TYPE,
106
+ type: MAIL_ACCOUNT_UPDATED_EVENT_QN,
107
+ payload: eventPayload,
108
+ headers,
109
+ });
89
110
 
90
- return { isSuccess: true as const, data: { accountId: payload.accountId } };
91
- },
92
- };
111
+ return { isSuccess: true as const, data: { accountId: payload.accountId } };
112
+ },
113
+ };
114
+ }