nolo-cli 0.1.13 → 0.1.14

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 (320) hide show
  1. package/README.md +9 -2
  2. package/agent-runtime/hostAdapter.ts +53 -0
  3. package/agent-runtime/index.ts +28 -0
  4. package/agent-runtime/localLoop.ts +62 -0
  5. package/agent-runtime/runtimeDecision.ts +70 -0
  6. package/agent-runtime/types.ts +87 -0
  7. package/agentRuntimeCommands.ts +139 -22
  8. package/agentRuntimeLocal.ts +7 -0
  9. package/ai/agent/_executeModel.ts +118 -0
  10. package/ai/agent/agentSlice.ts +544 -1
  11. package/ai/agent/appWorkingMemory.ts +126 -0
  12. package/ai/agent/avatarUtils.ts +24 -0
  13. package/ai/agent/buildEditingContext.ts +373 -0
  14. package/ai/agent/buildSystemPrompt.ts +532 -0
  15. package/ai/agent/cleanAgentMessages.ts +140 -0
  16. package/ai/agent/cliChatClient.ts +119 -0
  17. package/ai/agent/contextCompiler.ts +107 -0
  18. package/ai/agent/contextLayerContract.ts +44 -0
  19. package/ai/agent/createAgentSchema.ts +234 -0
  20. package/ai/agent/executeToolCall.ts +58 -0
  21. package/ai/agent/fetchAgentContexts.ts +42 -0
  22. package/ai/agent/generatePrompt.ts +3 -0
  23. package/ai/agent/getFullChatContextKeys.ts +168 -0
  24. package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
  25. package/ai/agent/hooks/useAgentConfig.ts +61 -0
  26. package/ai/agent/hooks/useAgentDialog.ts +35 -0
  27. package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
  28. package/ai/agent/hooks/usePublicAgents.ts +473 -0
  29. package/ai/agent/persistMessageWithFixedId.ts +37 -0
  30. package/ai/agent/planSlice.ts +259 -0
  31. package/ai/agent/referenceUtils.ts +229 -0
  32. package/ai/agent/runAgentBackground.ts +238 -0
  33. package/ai/agent/runAgentClientLoop.ts +138 -0
  34. package/ai/agent/runtimeGuidance.ts +97 -0
  35. package/ai/agent/runtimeServerBase.ts +37 -0
  36. package/ai/agent/server/fetchPublicAgents.ts +128 -0
  37. package/ai/agent/startParallelAgentStreams.ts +424 -0
  38. package/ai/agent/startupProtocol.ts +53 -0
  39. package/ai/agent/streamAgentChatTurn.ts +1299 -0
  40. package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
  41. package/ai/agent/types.ts +71 -0
  42. package/ai/agent/utils/imageOutput.ts +39 -0
  43. package/ai/agent/utils/publicImageAgentMode.ts +26 -0
  44. package/ai/agent/utils/sortUtils.ts +250 -0
  45. package/ai/agent/web/referencePickerUtils.ts +146 -0
  46. package/ai/ai.locale.ts +1083 -0
  47. package/ai/chat/accumulateToolCallChunks.ts +95 -0
  48. package/ai/chat/fetchUtils.native.ts +276 -0
  49. package/ai/chat/fetchUtils.ts +153 -0
  50. package/ai/chat/inlineImageUrlsForCustomProvider.ts +117 -0
  51. package/ai/chat/parseApiError.ts +64 -0
  52. package/ai/chat/parseMultilineSSE.ts +95 -0
  53. package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
  54. package/ai/chat/sendOpenAICompletionsRequest.ts +712 -0
  55. package/ai/chat/sendOpenAIResponseRequest.ts +512 -0
  56. package/ai/chat/shouldUseServerProxy.ts +18 -0
  57. package/ai/chat/sseClient.native.ts +91 -0
  58. package/ai/chat/sseClient.ts +67 -0
  59. package/ai/chat/streamReader.native.ts +31 -0
  60. package/ai/chat/streamReader.ts +62 -0
  61. package/ai/chat/updateTotalUsage.ts +72 -0
  62. package/ai/context/buildReferenceContext.ts +437 -0
  63. package/ai/context/calculateContextUsage.ts +133 -0
  64. package/ai/context/retention.ts +165 -0
  65. package/ai/context/tokenUtils.ts +78 -0
  66. package/ai/index.ts +1 -1
  67. package/ai/llm/agentCapabilities.ts +74 -0
  68. package/ai/llm/calculateGeminiImageTokens.ts +57 -0
  69. package/ai/llm/deepinfra.ts +28 -0
  70. package/ai/llm/fireworks.ts +68 -0
  71. package/ai/llm/generateRequestBody.ts +165 -0
  72. package/ai/llm/getModelContextWindow.ts +84 -0
  73. package/ai/llm/getNoloKey.ts +37 -0
  74. package/ai/llm/getPricing.ts +232 -0
  75. package/ai/llm/hooks/useModelPricing.ts +75 -0
  76. package/ai/llm/imagePricing.ts +66 -0
  77. package/ai/llm/isResponseAPIModel.ts +13 -0
  78. package/ai/llm/kimi.ts +18 -0
  79. package/ai/llm/mimo.ts +71 -0
  80. package/ai/llm/mistral.ts +22 -0
  81. package/ai/llm/modelAvatar.ts +427 -0
  82. package/ai/llm/models.ts +45 -0
  83. package/ai/llm/openrouterModels.ts +141 -0
  84. package/ai/llm/providers.ts +307 -0
  85. package/ai/llm/reasoningModels.ts +28 -0
  86. package/ai/llm/types.ts +59 -0
  87. package/ai/llm/usageRequestOptions.ts +59 -0
  88. package/ai/memory/capture.ts +148 -0
  89. package/ai/memory/consolidate.ts +104 -0
  90. package/ai/memory/delete.ts +147 -0
  91. package/ai/memory/overlay.ts +84 -0
  92. package/ai/memory/query.ts +38 -0
  93. package/ai/memory/queryShared.ts +160 -0
  94. package/ai/memory/rank.ts +105 -0
  95. package/ai/memory/recentRelationshipRecap.ts +247 -0
  96. package/ai/memory/remember.ts +167 -0
  97. package/ai/memory/runtime.ts +76 -0
  98. package/ai/memory/store.ts +20 -0
  99. package/ai/memory/storeShared.ts +76 -0
  100. package/ai/memory/types.ts +46 -0
  101. package/ai/memory/understanding.ts +349 -0
  102. package/ai/memory/understandingGreeting.ts +264 -0
  103. package/ai/messages/type.ts +20 -0
  104. package/ai/policy/personalizationDialog.ts +333 -0
  105. package/ai/policy/runtimePolicy.ts +440 -0
  106. package/ai/policy/selfUpdateFields.ts +48 -0
  107. package/ai/policy/types.ts +64 -0
  108. package/ai/skills/referenceRuntime.ts +274 -0
  109. package/ai/skills/skillDiagnostics.ts +251 -0
  110. package/ai/skills/skillDocBuilder.ts +139 -0
  111. package/ai/skills/skillDocProtocol.ts +434 -0
  112. package/ai/skills/skillReferenceSummary.ts +63 -0
  113. package/ai/skills/skillSummaryMarker.ts +26 -0
  114. package/ai/token/calculatePrice.ts +546 -0
  115. package/ai/token/db.ts +98 -0
  116. package/ai/token/externalToolCost.ts +321 -0
  117. package/ai/token/hooks/useRecords.ts +65 -0
  118. package/ai/token/missingUsageEstimate.ts +42 -0
  119. package/ai/token/modelUsageQuery.ts +252 -0
  120. package/ai/token/normalizeUsage.ts +84 -0
  121. package/ai/token/openaiImageGenerationUsage.ts +56 -0
  122. package/ai/token/prepareTokenUsageData.ts +88 -0
  123. package/ai/token/query.ts +88 -0
  124. package/ai/token/queryUserTokens.ts +59 -0
  125. package/ai/token/resolveBillingTarget.ts +52 -0
  126. package/ai/token/saveTokenRecord.ts +53 -0
  127. package/ai/token/serverDialogProjection.ts +78 -0
  128. package/ai/token/serverTokenWriter.ts +143 -0
  129. package/ai/token/stats.ts +21 -0
  130. package/ai/token/tokenThunks.ts +24 -0
  131. package/ai/token/types.ts +93 -0
  132. package/ai/tools/agent/agentTools.ts +176 -0
  133. package/ai/tools/agent/agentUpdateShared.ts +311 -0
  134. package/ai/tools/agent/callAgentTool.ts +139 -0
  135. package/ai/tools/agent/createAgentTool.ts +512 -0
  136. package/ai/tools/agent/createDialogTool.ts +69 -0
  137. package/ai/tools/agent/createSkillAgentTool.ts +62 -0
  138. package/ai/tools/agent/parallelBudget.ts +221 -0
  139. package/ai/tools/agent/presets/appBuilderPreset.ts +147 -0
  140. package/ai/tools/agent/runLlmTool.ts +96 -0
  141. package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
  142. package/ai/tools/agent/skillAgentArgs.ts +106 -0
  143. package/ai/tools/agent/skillAgentPreset.ts +89 -0
  144. package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
  145. package/ai/tools/agent/updateAgentTool.ts +96 -0
  146. package/ai/tools/agent/updateSelfTool.ts +113 -0
  147. package/ai/tools/amazonProductScraperTool.ts +86 -0
  148. package/ai/tools/apifyActorClient.ts +45 -0
  149. package/ai/tools/appEditGuard.ts +372 -0
  150. package/ai/tools/appReadSnapshot.ts +153 -0
  151. package/ai/tools/appTools.ts +1549 -0
  152. package/ai/tools/applyEditTool.ts +256 -0
  153. package/ai/tools/applyLineEditsTool.ts +312 -0
  154. package/ai/tools/browserTools/click.ts +33 -0
  155. package/ai/tools/browserTools/closeSession.ts +29 -0
  156. package/ai/tools/browserTools/common.ts +27 -0
  157. package/ai/tools/browserTools/openSession.ts +48 -0
  158. package/ai/tools/browserTools/readContent.ts +38 -0
  159. package/ai/tools/browserTools/selectOption.ts +46 -0
  160. package/ai/tools/browserTools/typeText.ts +42 -0
  161. package/ai/tools/category/createCategoryTool.ts +66 -0
  162. package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
  163. package/ai/tools/category/updateContentCategoryTool.ts +75 -0
  164. package/ai/tools/cfBrowserTools.ts +319 -0
  165. package/ai/tools/cfSpeechToTextTool.ts +49 -0
  166. package/ai/tools/checkEnvTool.ts +65 -0
  167. package/ai/tools/cloudflareCrawlTool.ts +289 -0
  168. package/ai/tools/codeSearchTool.ts +111 -0
  169. package/ai/tools/codeTools.ts +101 -0
  170. package/ai/tools/createDocTool.ts +132 -0
  171. package/ai/tools/createPlanTool.ts +999 -0
  172. package/ai/tools/createSkillDocTool.ts +155 -0
  173. package/ai/tools/createWorkflowTool.ts +154 -0
  174. package/ai/tools/deepseekOcrTool.ts +34 -0
  175. package/ai/tools/delayTool.ts +31 -0
  176. package/ai/tools/deleteSpacesTool.ts +325 -0
  177. package/ai/tools/deleteSpacesToolModel.ts +159 -0
  178. package/ai/tools/devReloadUtils.ts +29 -0
  179. package/ai/tools/dialogMessageSearch.ts +137 -0
  180. package/ai/tools/doctorSkillTool.ts +72 -0
  181. package/ai/tools/ecommerceScraperTool.ts +86 -0
  182. package/ai/tools/emailTools.ts +549 -0
  183. package/ai/tools/evalSkillTool.ts +92 -0
  184. package/ai/tools/exaSearchTool.ts +64 -0
  185. package/ai/tools/execBashTool.ts +379 -0
  186. package/ai/tools/executeSqlTool.ts +192 -0
  187. package/ai/tools/fetchWebpageSupport.ts +309 -0
  188. package/ai/tools/fetchWebpageTool.ts +84 -0
  189. package/ai/tools/geminiImagePreviewTool.ts +361 -0
  190. package/ai/tools/generateDocxTool.ts +215 -0
  191. package/ai/tools/googleSearchScraperTool.ts +106 -0
  192. package/ai/tools/importDataTool.ts +133 -0
  193. package/ai/tools/importSkillTool.ts +162 -0
  194. package/ai/tools/index.ts +1927 -0
  195. package/ai/tools/listFilesTool.ts +82 -0
  196. package/ai/tools/listUserSpacesTool.ts +113 -0
  197. package/ai/tools/modelUsageTools.ts +199 -0
  198. package/ai/tools/olmOcrTool.ts +34 -0
  199. package/ai/tools/openaiImageTool.ts +267 -0
  200. package/ai/tools/prepareTools.ts +23 -0
  201. package/ai/tools/readDocTool.ts +84 -0
  202. package/ai/tools/readFileTool.ts +211 -0
  203. package/ai/tools/readTool.ts +163 -0
  204. package/ai/tools/readXPostTool.ts +233 -0
  205. package/ai/tools/rememberMemoryTool.ts +84 -0
  206. package/ai/tools/remotionVideoTool.ts +151 -0
  207. package/ai/tools/searchDialogMessagesTool.ts +222 -0
  208. package/ai/tools/searchRepoTool.ts +115 -0
  209. package/ai/tools/searchWorkspaceTool.ts +259 -0
  210. package/ai/tools/skillFollowup.ts +86 -0
  211. package/ai/tools/surfWeatherTool.ts +169 -0
  212. package/ai/tools/table/addTableRowTool.ts +217 -0
  213. package/ai/tools/table/createTableTool.ts +315 -0
  214. package/ai/tools/table/rowTools.ts +366 -0
  215. package/ai/tools/table/schemaTools.ts +244 -0
  216. package/ai/tools/table/shareTableTool.ts +148 -0
  217. package/ai/tools/table/toolShared.ts +129 -0
  218. package/ai/tools/toolApiClient.ts +198 -0
  219. package/ai/tools/toolNameAliases.ts +57 -0
  220. package/ai/tools/toolResultError.ts +42 -0
  221. package/ai/tools/toolRunSlice.ts +303 -0
  222. package/ai/tools/toolSchemaCompatibility.ts +53 -0
  223. package/ai/tools/toolVisibility.ts +4 -0
  224. package/ai/tools/types.ts +20 -0
  225. package/ai/tools/uiAskChoiceTool.ts +104 -0
  226. package/ai/tools/updateContentTitleTool.ts +84 -0
  227. package/ai/tools/updateDocTool.ts +105 -0
  228. package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
  229. package/ai/tools/whisperTool.ts +77 -0
  230. package/ai/tools/writeFileTool.ts +210 -0
  231. package/ai/tools/youtubeScraperTool.ts +116 -0
  232. package/ai/tools/ziweiChartTool.ts +678 -0
  233. package/ai/types.ts +55 -0
  234. package/ai/workflow/workflowExecutor.ts +323 -0
  235. package/ai/workflow/workflowSlice.ts +73 -0
  236. package/ai/workflow/workflowTypes.ts +106 -0
  237. package/client/agentRun.test.ts +240 -0
  238. package/client/agentRun.ts +182 -19
  239. package/client/compactDialog.test.ts +238 -0
  240. package/client/localRuntimeAdapter.test.ts +135 -0
  241. package/client/localRuntimeAdapter.ts +244 -0
  242. package/client/profileConfig.test.ts +40 -0
  243. package/client/streamingOutput.test.ts +22 -0
  244. package/client/streamingOutput.ts +38 -0
  245. package/commandRegistry.ts +9 -2
  246. package/connector-experimental/index.ts +5 -0
  247. package/database/actions/cacheMergedUserData.ts +64 -0
  248. package/database/actions/common.ts +242 -0
  249. package/database/actions/deleteFile.ts +40 -0
  250. package/database/actions/fetchUserData.ts +16 -0
  251. package/database/actions/fileContent.ts +125 -0
  252. package/database/actions/patch.ts +155 -0
  253. package/database/actions/read.ts +337 -0
  254. package/database/actions/readAndWait.ts +224 -0
  255. package/database/actions/readRequestManager.ts +120 -0
  256. package/database/actions/remove.ts +94 -0
  257. package/database/actions/replication.ts +366 -0
  258. package/database/actions/upload.ts +174 -0
  259. package/database/actions/upsert.ts +56 -0
  260. package/database/actions/write.ts +126 -0
  261. package/database/client/db.native.ts +73 -0
  262. package/database/client/db.ts +51 -0
  263. package/database/client/fetchUserData.ts +61 -0
  264. package/database/client/handleError.ts +19 -0
  265. package/database/client/queryRequest.ts +21 -0
  266. package/database/config.ts +21 -0
  267. package/database/dbActionThunks.ts +1 -0
  268. package/database/dbSlice.ts +149 -0
  269. package/database/email.ts +42 -0
  270. package/database/fileRing.ts +51 -0
  271. package/database/fileSharding.ts +70 -0
  272. package/database/fileStorage.native.ts +92 -0
  273. package/database/fileStorage.ts +232 -0
  274. package/database/fileUrl.ts +34 -0
  275. package/database/hooks/useUserData.ts +489 -0
  276. package/database/index.ts +1 -0
  277. package/database/keys.ts +765 -0
  278. package/database/queryPrefixes.ts +14 -0
  279. package/database/requests.ts +443 -0
  280. package/database/runtimeServerContext.ts +35 -0
  281. package/database/server/MemoryDB.ts +76 -0
  282. package/database/server/actorAccess.ts +76 -0
  283. package/database/server/agentDelegation.ts +124 -0
  284. package/database/server/coreDataOwnership.ts +13 -0
  285. package/database/server/coreDataProxy.ts +76 -0
  286. package/database/server/cybotReadonly.ts +18 -0
  287. package/database/server/dataHandlers.ts +111 -0
  288. package/database/server/db.ts +118 -0
  289. package/database/server/dbPath.ts +20 -0
  290. package/database/server/delete.ts +499 -0
  291. package/database/server/emailRepository.ts +1480 -0
  292. package/database/server/ensureDbOpen.ts +12 -0
  293. package/database/server/fileRead.ts +337 -0
  294. package/database/server/fileService.ts +436 -0
  295. package/database/server/handleTransaction.ts +86 -0
  296. package/database/server/patch.ts +282 -0
  297. package/database/server/query.ts +138 -0
  298. package/database/server/read.ts +325 -0
  299. package/database/server/resourceAccess.ts +211 -0
  300. package/database/server/routes.ts +110 -0
  301. package/database/server/spaceMemberAuthority.ts +67 -0
  302. package/database/server/upload.ts +159 -0
  303. package/database/server/write.ts +494 -0
  304. package/database/server/writeAuthority.ts +133 -0
  305. package/database/sqliteDb.ts +46 -0
  306. package/database/table/deleteTable.ts +120 -0
  307. package/database/tenantPlacement.ts +57 -0
  308. package/database/tombstones.ts +52 -0
  309. package/database/userDataLoadDecision.ts +17 -0
  310. package/database/userDataMerge.ts +95 -0
  311. package/database/userPreferenceRegister.ts +108 -0
  312. package/database/utils/dbPath.ts +47 -0
  313. package/database/utils/ulid.native.ts +6 -0
  314. package/database/utils/ulid.ts +1 -0
  315. package/index.ts +25 -15
  316. package/localRuntimeDb.ts +28 -0
  317. package/package.json +16 -4
  318. package/runtimeModeArgs.ts +33 -0
  319. package/tui/readlineWorkspace.ts +1 -0
  320. package/tui/session.ts +22 -0
@@ -0,0 +1,1480 @@
1
+ import type { ActorContext } from "auth/actor";
2
+ import { DataType } from "create/types";
3
+ import type {
4
+ EmailMailbox,
5
+ EmailParticipant,
6
+ EmailRecord,
7
+ EmailStatus,
8
+ } from "database/email";
9
+ import { emailKey } from "database/keys";
10
+ import type { ApiContext, ApiError } from "server/api/types";
11
+ import {
12
+ type EmailProvider,
13
+ type EmailProviderSendResult,
14
+ } from "server/email/provider";
15
+ import { getEmailProvider } from "server/email/providerRegistry";
16
+ import { authorizeActorRecordAccess } from "./actorAccess";
17
+ import { agentDelegationKey } from "./agentDelegation";
18
+ import serverDb from "./db";
19
+ import { resolveKeyOwnerId } from "./writeAuthority";
20
+
21
+ type CreateEmailRecordInput = {
22
+ ownerId: string;
23
+ ownerType?: EmailRecord["ownerType"];
24
+ tenantId?: string;
25
+ spaceId?: string | null;
26
+ mailbox?: EmailMailbox;
27
+ status?: EmailStatus;
28
+ from: EmailParticipant;
29
+ to?: EmailParticipant[];
30
+ cc?: EmailParticipant[];
31
+ bcc?: EmailParticipant[];
32
+ replyTo?: EmailParticipant[];
33
+ subject?: string;
34
+ text?: string;
35
+ html?: string;
36
+ messageId?: string;
37
+ threadId?: string;
38
+ inReplyTo?: string;
39
+ references?: string[];
40
+ tags?: string[];
41
+ meta?: Record<string, unknown>;
42
+ now?: string | number;
43
+ };
44
+
45
+ type ListEmailsParams = {
46
+ ownerId?: string;
47
+ mailbox?: EmailMailbox;
48
+ status?: EmailStatus;
49
+ tag?: string;
50
+ limit?: number;
51
+ };
52
+
53
+ type EmailKeyParams = {
54
+ dbKey: string;
55
+ };
56
+
57
+ type UpdateEmailTagsParams = EmailKeyParams & {
58
+ tags: string[];
59
+ };
60
+
61
+ type SendEmailParams = {
62
+ agentId: string;
63
+ from: EmailParticipant;
64
+ to: EmailParticipant[];
65
+ cc?: EmailParticipant[];
66
+ bcc?: EmailParticipant[];
67
+ replyTo?: EmailParticipant[];
68
+ subject: string;
69
+ text?: string;
70
+ html?: string;
71
+ };
72
+
73
+ type ReceiveInboundEmailResult = {
74
+ records: EmailRecord[];
75
+ existing: EmailRecord[];
76
+ };
77
+
78
+ type MirrorEmailToControlledInboxesParams = {
79
+ from: EmailParticipant;
80
+ to?: EmailParticipant[];
81
+ cc?: EmailParticipant[];
82
+ bcc?: EmailParticipant[];
83
+ replyTo?: EmailParticipant[];
84
+ subject?: string;
85
+ text?: string;
86
+ html?: string;
87
+ providerName: string;
88
+ providerMessageId?: string;
89
+ providerReceivedAt?: string | number;
90
+ rawHeaders?: unknown;
91
+ raw?: unknown;
92
+ meta?: Record<string, unknown>;
93
+ };
94
+
95
+ type BindAgentEmailIdentityParams = {
96
+ agentId: string;
97
+ emailAddress: string;
98
+ provider?: string;
99
+ };
100
+
101
+ type ProvisionAgentEmailIdentityParams = {
102
+ agentId: string;
103
+ purpose?: string;
104
+ localPart?: string;
105
+ domain?: string;
106
+ provider?: string;
107
+ makePrimary?: boolean;
108
+ };
109
+
110
+ type AgentEmailIdentity = {
111
+ emailAddress: string;
112
+ provider?: string;
113
+ purpose?: string;
114
+ createdAt?: string | number;
115
+ verifiedAt?: string | number;
116
+ readinessStatus?: "created" | "warming" | "ready" | "failed_warmup";
117
+ ingressReadyAt?: string | number | null;
118
+ lastWarmupAt?: string | number | null;
119
+ lastWarmupError?: string | null;
120
+ disabledAt?: string | number | null;
121
+ source?: "bound" | "provisioned";
122
+ };
123
+
124
+ const MAX_LIST_LIMIT = 200;
125
+ const DEFAULT_AGENT_EMAIL_PROVIDER = "cloudflare";
126
+ const DEFAULT_CLOUDFLARE_EMAIL_ROUTING_WORKER = "bun-nolo-agent-email-router";
127
+ const DEFAULT_CLOUDFLARE_EMAIL_ROUTE_READY_TIMEOUT_MS = 60000;
128
+ const DEFAULT_CLOUDFLARE_EMAIL_ROUTE_READY_POLL_MS = 2500;
129
+
130
+ const apiError = (code: ApiError["code"], message: string): ApiError => ({
131
+ code,
132
+ message,
133
+ });
134
+
135
+ const normalizeTags = (tags?: unknown): string[] => {
136
+ if (!Array.isArray(tags)) return [];
137
+ return Array.from(
138
+ new Set(
139
+ tags
140
+ .map((tag) => (typeof tag === "string" ? tag.trim() : ""))
141
+ .filter(Boolean)
142
+ )
143
+ );
144
+ };
145
+
146
+ const actorOwnerId = (actor?: ActorContext | null): string | null => {
147
+ if (!actor) return null;
148
+ if (actor.type === "user") return actor.userId;
149
+ return actor.principalUserId || null;
150
+ };
151
+
152
+ const requireActor = (context?: ApiContext): ActorContext => {
153
+ const actor = context?.actor;
154
+ if (!actor) throw apiError("unauthorized", "Authentication required");
155
+ return actor;
156
+ };
157
+
158
+ const assertEmailAccess = async (
159
+ actor: ActorContext,
160
+ action: "read" | "manage" | "write",
161
+ record: EmailRecord
162
+ ) => {
163
+ const access = await authorizeActorRecordAccess({
164
+ action,
165
+ actor,
166
+ dbKey: record.dbKey,
167
+ record,
168
+ });
169
+ if (!access.allowed) {
170
+ throw apiError("forbidden", "You do not have access to this email");
171
+ }
172
+ };
173
+
174
+ const loadEmailRecord = async (dbKey: string): Promise<EmailRecord> => {
175
+ if (!dbKey) throw apiError("invalid_input", "dbKey is required");
176
+
177
+ const record = await serverDb.get(dbKey).catch(() => null);
178
+ if (!record || record.type !== DataType.EMAIL || record.deletedAt) {
179
+ throw apiError("not_found", "Email not found");
180
+ }
181
+ return record as EmailRecord;
182
+ };
183
+
184
+ const normalizeEmailAddress = (value?: unknown): string =>
185
+ typeof value === "string" ? value.trim().toLowerCase() : "";
186
+
187
+ const normalizeParticipants = (value?: unknown): EmailParticipant[] => {
188
+ if (!Array.isArray(value)) return [];
189
+ return value
190
+ .map((item) => {
191
+ if (!item || typeof item !== "object") return null;
192
+ const raw = item as Record<string, unknown>;
193
+ const email = normalizeEmailAddress(raw.email);
194
+ if (!email) return null;
195
+ const name =
196
+ typeof raw.name === "string" && raw.name.trim()
197
+ ? raw.name.trim()
198
+ : undefined;
199
+ return { email, ...(name ? { name } : {}) };
200
+ })
201
+ .filter((item): item is EmailParticipant => Boolean(item));
202
+ };
203
+
204
+ const normalizeEmailDomain = (value?: unknown): string =>
205
+ typeof value === "string"
206
+ ? value.trim().toLowerCase().replace(/^@+/, "")
207
+ : "";
208
+
209
+ const normalizeLocalPart = (value?: unknown): string =>
210
+ typeof value === "string" ? value.trim().toLowerCase() : "";
211
+
212
+ const normalizePurpose = (value?: unknown): string => {
213
+ if (typeof value !== "string") return "";
214
+ return value
215
+ .trim()
216
+ .toLowerCase()
217
+ .replace(/[^a-z0-9]+/g, "-")
218
+ .replace(/^-+|-+$/g, "")
219
+ .slice(0, 32);
220
+ };
221
+
222
+ const hashToken = (value: string): string => {
223
+ let hash = 2166136261;
224
+ for (let index = 0; index < value.length; index += 1) {
225
+ hash ^= value.charCodeAt(index);
226
+ hash = Math.imul(hash, 16777619);
227
+ }
228
+ return (hash >>> 0).toString(36).padStart(6, "0").slice(0, 8);
229
+ };
230
+
231
+ const PROVISIONED_EMAIL_FIRST_NAMES = [
232
+ "ava",
233
+ "clara",
234
+ "ella",
235
+ "grace",
236
+ "iris",
237
+ "julia",
238
+ "lena",
239
+ "maya",
240
+ "nina",
241
+ "olive",
242
+ "sara",
243
+ "tessa",
244
+ "leo",
245
+ "milo",
246
+ "owen",
247
+ "theo",
248
+ ] as const;
249
+
250
+ const PROVISIONED_EMAIL_LAST_NAMES = [
251
+ "bennett",
252
+ "brooks",
253
+ "carter",
254
+ "ellis",
255
+ "foster",
256
+ "hayes",
257
+ "jensen",
258
+ "morris",
259
+ "parker",
260
+ "quinn",
261
+ "reed",
262
+ "sawyer",
263
+ "turner",
264
+ "walker",
265
+ "wright",
266
+ "young",
267
+ ] as const;
268
+
269
+ const splitConfiguredDomains = (value?: string): string[] =>
270
+ String(value || "")
271
+ .split(",")
272
+ .map(normalizeEmailDomain)
273
+ .filter(Boolean);
274
+
275
+ const extractDomainFromEmailValue = (value?: string): string => {
276
+ const raw = String(value || "").trim().toLowerCase();
277
+ if (!raw) return "";
278
+ const match = raw.match(/<?([^<>\s]+@[^<>\s]+)>?/);
279
+ if (!match?.[1]) return "";
280
+ const [, domain = ""] = match[1].split("@");
281
+ return normalizeEmailDomain(domain);
282
+ };
283
+
284
+ const configuredAgentEmailDomains = (): string[] => {
285
+ const domains = [
286
+ ...splitConfiguredDomains(process.env.AGENT_EMAIL_DOMAINS),
287
+ ...splitConfiguredDomains(process.env.AGENT_EMAIL_DOMAIN),
288
+ ...splitConfiguredDomains(process.env.EMAIL_AGENT_DOMAINS),
289
+ ...splitConfiguredDomains(process.env.EMAIL_AGENT_DOMAIN),
290
+ ...splitConfiguredDomains(process.env.EMAIL_INBOUND_DOMAINS),
291
+ ...splitConfiguredDomains(process.env.EMAIL_INBOUND_DOMAIN),
292
+ extractDomainFromEmailValue(process.env.AGENT_EMAIL_E2E_FROM),
293
+ extractDomainFromEmailValue(process.env.CLOUDFLARE_EMAIL_FROM),
294
+ extractDomainFromEmailValue(process.env.EMAIL_FROM),
295
+ extractDomainFromEmailValue(process.env.RESEND_FROM_EMAIL),
296
+ ];
297
+ return Array.from(new Set(domains.filter(Boolean)));
298
+ };
299
+
300
+ const resolveAgentEmailDomain = (requestedDomain?: unknown): string => {
301
+ const configured = configuredAgentEmailDomains();
302
+ const requested = normalizeEmailDomain(requestedDomain);
303
+ if (requested) {
304
+ if (configured.length === 0) {
305
+ throw apiError(
306
+ "invalid_input",
307
+ "AGENT_EMAIL_DOMAIN or AGENT_EMAIL_DOMAINS must be configured"
308
+ );
309
+ }
310
+ if (configured.length > 0 && !configured.includes(requested)) {
311
+ throw apiError("forbidden", "Requested email domain is not allowed");
312
+ }
313
+ return requested;
314
+ }
315
+ const first = configured[0];
316
+ if (!first) {
317
+ throw apiError(
318
+ "invalid_input",
319
+ "AGENT_EMAIL_DOMAIN or AGENT_EMAIL_DOMAINS must be configured"
320
+ );
321
+ }
322
+ return first;
323
+ };
324
+
325
+ const normalizeProviderName = (value?: unknown): string =>
326
+ typeof value === "string" ? value.trim().toLowerCase() : "";
327
+
328
+ const resolveCloudflareEmailRoutingConfig = () => {
329
+ const zoneId =
330
+ process.env.CLOUDFLARE_EMAIL_ROUTING_ZONE_ID?.trim() ||
331
+ process.env.CLOUDFLARE_ZONE_ID?.trim() ||
332
+ "";
333
+ const apiToken =
334
+ process.env.CLOUDFLARE_EMAIL_ROUTING_API_TOKEN?.trim() ||
335
+ process.env.CLOUDFLARE_API_TOKEN?.trim() ||
336
+ "";
337
+ const workerName =
338
+ process.env.CLOUDFLARE_EMAIL_ROUTING_WORKER_NAME?.trim() ||
339
+ DEFAULT_CLOUDFLARE_EMAIL_ROUTING_WORKER;
340
+ if (!zoneId || !apiToken || !workerName) return null;
341
+ return {
342
+ zoneId,
343
+ apiToken,
344
+ workerName,
345
+ apiBaseUrl:
346
+ process.env.CLOUDFLARE_EMAIL_ROUTING_API_BASE_URL?.trim() ||
347
+ "https://api.cloudflare.com/client/v4",
348
+ };
349
+ };
350
+
351
+ const parseCloudflareApiJson = async (response: Response) => {
352
+ const text = await response.text();
353
+ try {
354
+ return text ? JSON.parse(text) : null;
355
+ } catch {
356
+ return { rawText: text };
357
+ }
358
+ };
359
+
360
+ const cloudflareApiErrorMessage = (payload: any, fallback: string): string => {
361
+ const errors = Array.isArray(payload?.errors)
362
+ ? payload.errors
363
+ .map((error: any) => error?.message || error?.code)
364
+ .filter(Boolean)
365
+ : [];
366
+ return errors.length > 0 ? errors.join("; ") : fallback;
367
+ };
368
+
369
+ const isMatchingCloudflareWorkerRoute = (
370
+ rule: any,
371
+ emailAddress: string,
372
+ workerName: string
373
+ ) => {
374
+ const matchers = Array.isArray(rule?.matchers) ? rule.matchers : [];
375
+ const matcher = matchers.find(
376
+ (item: any) =>
377
+ item?.type === "literal" &&
378
+ item?.field === "to" &&
379
+ normalizeEmailAddress(item?.value) === emailAddress
380
+ );
381
+ if (!matcher) return false;
382
+ const actions = Array.isArray(rule?.actions) ? rule.actions : [];
383
+ return actions.some(
384
+ (action: any) =>
385
+ action?.type === "worker" &&
386
+ Array.isArray(action?.value) &&
387
+ action.value.includes(workerName)
388
+ );
389
+ };
390
+
391
+ const hasConflictingCloudflareRoute = (rule: any, emailAddress: string): boolean => {
392
+ const matchers = Array.isArray(rule?.matchers) ? rule.matchers : [];
393
+ return matchers.some(
394
+ (item: any) =>
395
+ item?.type === "literal" &&
396
+ item?.field === "to" &&
397
+ normalizeEmailAddress(item?.value) === emailAddress
398
+ );
399
+ };
400
+
401
+ const ensureCloudflareEmailRoutingRule = async (
402
+ emailAddress: string,
403
+ provider?: unknown
404
+ ) => {
405
+ if (normalizeProviderName(provider) !== "cloudflare") return;
406
+ const config = resolveCloudflareEmailRoutingConfig();
407
+ if (!config) return;
408
+
409
+ const url = `${config.apiBaseUrl.replace(/\/+$/, "")}/zones/${encodeURIComponent(
410
+ config.zoneId
411
+ )}/email/routing/rules`;
412
+ const headers = {
413
+ Authorization: `Bearer ${config.apiToken}`,
414
+ "Content-Type": "application/json",
415
+ };
416
+
417
+ const listResponse = await fetch(url, {
418
+ method: "GET",
419
+ headers,
420
+ });
421
+ const listPayload = await parseCloudflareApiJson(listResponse);
422
+ if (!listResponse.ok || listPayload?.success === false) {
423
+ throw apiError(
424
+ "internal_error",
425
+ `Failed to list Cloudflare email routing rules for ${emailAddress}: ${cloudflareApiErrorMessage(
426
+ listPayload,
427
+ `HTTP ${listResponse.status}`
428
+ )}`
429
+ );
430
+ }
431
+
432
+ const existingRules = Array.isArray(listPayload?.result) ? listPayload.result : [];
433
+ const matchingRule = existingRules.find((rule: any) =>
434
+ isMatchingCloudflareWorkerRoute(rule, emailAddress, config.workerName)
435
+ );
436
+ if (matchingRule && matchingRule.enabled !== false) {
437
+ return;
438
+ }
439
+
440
+ const conflictingRule = existingRules.find((rule: any) =>
441
+ hasConflictingCloudflareRoute(rule, emailAddress)
442
+ );
443
+ if (conflictingRule) {
444
+ throw apiError(
445
+ "invalid_input",
446
+ `Cloudflare email routing already has a conflicting rule for ${emailAddress}`
447
+ );
448
+ }
449
+
450
+ const createResponse = await fetch(url, {
451
+ method: "POST",
452
+ headers,
453
+ body: JSON.stringify({
454
+ name: `Agent email route ${emailAddress}`,
455
+ enabled: true,
456
+ priority: 0,
457
+ matchers: [
458
+ {
459
+ type: "literal",
460
+ field: "to",
461
+ value: emailAddress,
462
+ },
463
+ ],
464
+ actions: [
465
+ {
466
+ type: "worker",
467
+ value: [config.workerName],
468
+ },
469
+ ],
470
+ }),
471
+ });
472
+ const createPayload = await parseCloudflareApiJson(createResponse);
473
+ if (!createResponse.ok || createPayload?.success === false) {
474
+ throw apiError(
475
+ "internal_error",
476
+ `Failed to create Cloudflare email routing rule for ${emailAddress}: ${cloudflareApiErrorMessage(
477
+ createPayload,
478
+ `HTTP ${createResponse.status}`
479
+ )}`
480
+ );
481
+ }
482
+ };
483
+
484
+ const resolveRouteReadinessSender = (domain: string): EmailParticipant => {
485
+ const configured = [
486
+ process.env.AGENT_EMAIL_E2E_FROM,
487
+ process.env.CLOUDFLARE_EMAIL_FROM,
488
+ process.env.EMAIL_FROM,
489
+ process.env.RESEND_FROM_EMAIL,
490
+ ]
491
+ .map((item) => String(item || "").trim())
492
+ .find(Boolean);
493
+ const normalized = normalizeEmailAddress(configured);
494
+ if (normalized) {
495
+ return { email: normalized };
496
+ }
497
+ return { email: `no-reply@${domain}` };
498
+ };
499
+
500
+ const parsePositiveInt = (value: unknown, fallback: number): number => {
501
+ const parsed = Number.parseInt(String(value ?? ""), 10);
502
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
503
+ return parsed;
504
+ };
505
+
506
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
507
+
508
+ const findRecentInboundEmail = async (
509
+ agentId: string,
510
+ matcher: (record: EmailRecord) => boolean
511
+ ): Promise<EmailRecord | null> => {
512
+ const range = emailKey.rangeOfOwner(agentId);
513
+ for await (const [, value] of serverDb.iterator({
514
+ gte: range.start,
515
+ lte: range.end,
516
+ reverse: true,
517
+ })) {
518
+ const record = value as EmailRecord | null;
519
+ if (!record || record.type !== DataType.EMAIL || (record as any).deletedAt) continue;
520
+ if (matcher(record)) return record;
521
+ }
522
+ return null;
523
+ };
524
+
525
+ const archiveRouteReadinessProbe = async (record: EmailRecord) => {
526
+ await serverDb.put(record.dbKey, {
527
+ ...record,
528
+ mailbox: "archive",
529
+ tags: normalizeTags([...(record.tags || []), "route-readiness-probe"]),
530
+ updatedAt: new Date().toISOString(),
531
+ meta: {
532
+ ...(record.meta || {}),
533
+ routeReadinessProbe: true,
534
+ },
535
+ });
536
+ };
537
+
538
+ const waitForCloudflareRouteReadiness = async (
539
+ agentId: string,
540
+ emailAddress: string,
541
+ domain: string,
542
+ provider?: unknown
543
+ ): Promise<string> => {
544
+ if (normalizeProviderName(provider) !== "cloudflare") {
545
+ return new Date().toISOString();
546
+ }
547
+ if (!resolveCloudflareEmailRoutingConfig()) {
548
+ return new Date().toISOString();
549
+ }
550
+
551
+ const readinessEnabled = String(process.env.CLOUDFLARE_EMAIL_ROUTE_READY_ENABLED || "1")
552
+ .trim()
553
+ .toLowerCase();
554
+ if (readinessEnabled === "0" || readinessEnabled === "false") {
555
+ return new Date().toISOString();
556
+ }
557
+
558
+ const timeoutMs = parsePositiveInt(
559
+ process.env.CLOUDFLARE_EMAIL_ROUTE_READY_TIMEOUT_MS,
560
+ DEFAULT_CLOUDFLARE_EMAIL_ROUTE_READY_TIMEOUT_MS
561
+ );
562
+ const pollMs = parsePositiveInt(
563
+ process.env.CLOUDFLARE_EMAIL_ROUTE_READY_POLL_MS,
564
+ DEFAULT_CLOUDFLARE_EMAIL_ROUTE_READY_POLL_MS
565
+ );
566
+ const providerClient = getEmailProvider();
567
+ const now = Date.now();
568
+ const probeToken = `${now.toString(36)}-${hashToken(`${agentId}:${emailAddress}:${now}`)}`;
569
+ const subject = `Agent email route readiness ${probeToken}`;
570
+ const text = `route-readiness-probe:${probeToken}`;
571
+ const startedAt = Date.now();
572
+
573
+ await providerClient.sendEmail({
574
+ from: resolveRouteReadinessSender(domain),
575
+ to: [{ email: emailAddress }],
576
+ subject,
577
+ text,
578
+ headers: {
579
+ "X-Nolo-Route-Readiness": probeToken,
580
+ },
581
+ });
582
+
583
+ while (Date.now() - startedAt <= timeoutMs) {
584
+ const matched = await findRecentInboundEmail(agentId, (record) => {
585
+ if (record.mailbox !== "inbox") return false;
586
+ if (record.subject !== subject) return false;
587
+ if (!record.to?.some((item) => normalizeEmailAddress(item?.email) === emailAddress)) {
588
+ return false;
589
+ }
590
+ const createdAt = Number(new Date(record.createdAt).getTime());
591
+ return Number.isFinite(createdAt) ? createdAt >= startedAt - 1000 : true;
592
+ });
593
+ if (matched) {
594
+ await archiveRouteReadinessProbe(matched);
595
+ return new Date().toISOString();
596
+ }
597
+ await sleep(Math.min(pollMs, timeoutMs));
598
+ }
599
+
600
+ throw apiError(
601
+ "internal_error",
602
+ `Cloudflare email route for ${emailAddress} was created but did not become ingress-ready in time`
603
+ );
604
+ };
605
+
606
+ const assertValidLocalPart = (localPart: string) => {
607
+ if (
608
+ localPart.length < 1 ||
609
+ localPart.length > 64 ||
610
+ !/^[a-z0-9][a-z0-9._+-]*[a-z0-9]$|^[a-z0-9]$/.test(localPart)
611
+ ) {
612
+ throw apiError(
613
+ "invalid_input",
614
+ "localPart must be 1-64 chars and contain only letters, numbers, dot, underscore, plus, or hyphen"
615
+ );
616
+ }
617
+ };
618
+
619
+ const buildProvisionedLocalPart = (agentId: string, purpose?: unknown): string => {
620
+ const purposeSlug = normalizePurpose(purpose);
621
+ const token = hashToken(`${agentId}:${purposeSlug || "primary"}`);
622
+ const firstHash = Number.parseInt(hashToken(`${token}:first`), 36);
623
+ const lastHash = Number.parseInt(hashToken(`${token}:last`), 36);
624
+ const firstName =
625
+ PROVISIONED_EMAIL_FIRST_NAMES[firstHash % PROVISIONED_EMAIL_FIRST_NAMES.length];
626
+ const lastName =
627
+ PROVISIONED_EMAIL_LAST_NAMES[lastHash % PROVISIONED_EMAIL_LAST_NAMES.length];
628
+ const suffix = token.slice(-4);
629
+ return `${firstName}.${lastName}-${suffix}`;
630
+ };
631
+
632
+ const getAgentEmailIdentities = (agent: any): AgentEmailIdentity[] => {
633
+ const identities: AgentEmailIdentity[] = [];
634
+ const seen = new Set<string>();
635
+ const push = (identity: Partial<AgentEmailIdentity> | null | undefined) => {
636
+ const emailAddress = normalizeEmailAddress(identity?.emailAddress);
637
+ if (!emailAddress || seen.has(emailAddress) || identity?.disabledAt) return;
638
+ seen.add(emailAddress);
639
+ identities.push({
640
+ ...identity,
641
+ emailAddress,
642
+ });
643
+ };
644
+
645
+ push({
646
+ emailAddress: agent?.meta?.emailAddress,
647
+ provider: agent?.meta?.emailProvider,
648
+ verifiedAt: agent?.meta?.emailVerifiedAt,
649
+ readinessStatus: agent?.meta?.emailReadinessStatus,
650
+ ingressReadyAt: agent?.meta?.emailIngressReadyAt,
651
+ lastWarmupAt: agent?.meta?.emailLastWarmupAt,
652
+ lastWarmupError: agent?.meta?.emailLastWarmupError,
653
+ source: "bound",
654
+ });
655
+
656
+ if (Array.isArray(agent?.meta?.emailIdentities)) {
657
+ agent.meta.emailIdentities.forEach((identity: any) => push(identity));
658
+ }
659
+
660
+ return identities;
661
+ };
662
+
663
+ const hasAgentEmailIdentity = (agent: any, emailAddress: string): boolean => {
664
+ const normalized = normalizeEmailAddress(emailAddress);
665
+ return getAgentEmailIdentities(agent).some(
666
+ (identity) => identity.emailAddress === normalized
667
+ );
668
+ };
669
+
670
+ const getAgentEmailIdentity = (
671
+ agent: any,
672
+ emailAddress: string
673
+ ): AgentEmailIdentity | undefined => {
674
+ const normalized = normalizeEmailAddress(emailAddress);
675
+ return getAgentEmailIdentities(agent).find(
676
+ (identity) => identity.emailAddress === normalized
677
+ );
678
+ };
679
+
680
+ const withReadinessState = (
681
+ identity: AgentEmailIdentity,
682
+ patch: Partial<
683
+ Pick<
684
+ AgentEmailIdentity,
685
+ "readinessStatus" | "ingressReadyAt" | "lastWarmupAt" | "lastWarmupError"
686
+ >
687
+ >
688
+ ): AgentEmailIdentity => ({
689
+ ...identity,
690
+ ...patch,
691
+ });
692
+
693
+ const assertValidEmailAddress = (emailAddress: string) => {
694
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailAddress)) {
695
+ throw apiError("invalid_input", "emailAddress must be a valid email address");
696
+ }
697
+ };
698
+
699
+ const upsertAgentEmailIdentity = (
700
+ agent: any,
701
+ identity: AgentEmailIdentity,
702
+ options: { makePrimary?: boolean } = {}
703
+ ) => {
704
+ const emailAddress = normalizeEmailAddress(identity.emailAddress);
705
+ assertValidEmailAddress(emailAddress);
706
+ const now = new Date().toISOString();
707
+ const existingIdentities = Array.isArray(agent?.meta?.emailIdentities)
708
+ ? agent.meta.emailIdentities
709
+ : [];
710
+ const nextIdentity: AgentEmailIdentity = {
711
+ ...identity,
712
+ emailAddress,
713
+ createdAt: identity.createdAt || now,
714
+ verifiedAt: identity.verifiedAt || now,
715
+ };
716
+ const nextIdentities = [
717
+ nextIdentity,
718
+ ...existingIdentities.filter(
719
+ (item: any) => normalizeEmailAddress(item?.emailAddress) !== emailAddress
720
+ ),
721
+ ];
722
+ const currentPrimaryEmail = normalizeEmailAddress(agent?.meta?.emailAddress);
723
+ const shouldMakePrimary =
724
+ options.makePrimary ?? (!currentPrimaryEmail || currentPrimaryEmail === emailAddress);
725
+
726
+ return {
727
+ ...agent,
728
+ meta: {
729
+ ...(agent.meta || {}),
730
+ emailAddress: shouldMakePrimary
731
+ ? emailAddress
732
+ : normalizeEmailAddress(agent?.meta?.emailAddress) || emailAddress,
733
+ emailProvider:
734
+ shouldMakePrimary || !agent?.meta?.emailProvider
735
+ ? nextIdentity.provider || agent?.meta?.emailProvider || DEFAULT_AGENT_EMAIL_PROVIDER
736
+ : agent.meta.emailProvider,
737
+ emailVerifiedAt:
738
+ shouldMakePrimary || !agent?.meta?.emailVerifiedAt
739
+ ? nextIdentity.verifiedAt
740
+ : agent.meta.emailVerifiedAt,
741
+ emailIngressReadyAt:
742
+ shouldMakePrimary || !agent?.meta?.emailIngressReadyAt
743
+ ? nextIdentity.ingressReadyAt
744
+ : agent.meta.emailIngressReadyAt,
745
+ emailReadinessStatus:
746
+ shouldMakePrimary || !agent?.meta?.emailReadinessStatus
747
+ ? nextIdentity.readinessStatus
748
+ : agent.meta.emailReadinessStatus,
749
+ emailLastWarmupAt:
750
+ shouldMakePrimary || !agent?.meta?.emailLastWarmupAt
751
+ ? nextIdentity.lastWarmupAt
752
+ : agent.meta.emailLastWarmupAt,
753
+ emailLastWarmupError:
754
+ shouldMakePrimary || !agent?.meta?.emailLastWarmupError
755
+ ? nextIdentity.lastWarmupError
756
+ : agent.meta.emailLastWarmupError,
757
+ emailIdentities: nextIdentities,
758
+ },
759
+ updatedAt: now,
760
+ };
761
+ };
762
+
763
+ const buildBindIdentityResult = (
764
+ agentId: string,
765
+ emailAddress: string,
766
+ agent: any
767
+ ) => {
768
+ const identity = getAgentEmailIdentity(agent, emailAddress);
769
+ return {
770
+ agentId,
771
+ emailAddress,
772
+ readinessStatus: identity?.readinessStatus || null,
773
+ ingressReadyAt: identity?.ingressReadyAt || null,
774
+ lastWarmupAt: identity?.lastWarmupAt || null,
775
+ lastWarmupError: identity?.lastWarmupError || null,
776
+ agent,
777
+ delegationResourcePrefix: `email-${agentId}-`,
778
+ };
779
+ };
780
+
781
+ const buildProvisionIdentityResult = (
782
+ agentId: string,
783
+ emailAddress: string,
784
+ localPart: string,
785
+ domain: string,
786
+ provider: string,
787
+ purpose: string | null,
788
+ agent: any
789
+ ) => {
790
+ const identity = getAgentEmailIdentity(agent, emailAddress);
791
+ return {
792
+ agentId,
793
+ emailAddress,
794
+ localPart,
795
+ domain,
796
+ provider,
797
+ purpose,
798
+ readinessStatus: identity?.readinessStatus || null,
799
+ ingressReadyAt: identity?.ingressReadyAt || null,
800
+ lastWarmupAt: identity?.lastWarmupAt || null,
801
+ lastWarmupError: identity?.lastWarmupError || null,
802
+ agent,
803
+ delegationResourcePrefix: `email-${agentId}-`,
804
+ };
805
+ };
806
+
807
+ const loadOwnedAgent = async (
808
+ agentId: string,
809
+ principalUserId: string
810
+ ): Promise<any> => {
811
+ if (!agentId?.trim()) throw apiError("invalid_input", "agentId is required");
812
+ const agent = await serverDb.get(agentId).catch(() => null);
813
+ if (!agent || agent.deletedAt) throw apiError("not_found", "Agent not found");
814
+
815
+ const access = await authorizeActorRecordAccess({
816
+ action: "read",
817
+ actor: { type: "user", userId: principalUserId },
818
+ dbKey: agentId,
819
+ record: agent,
820
+ });
821
+ if (!access.allowed) {
822
+ throw apiError("forbidden", "You do not own this agent");
823
+ }
824
+ return agent;
825
+ };
826
+
827
+ export const findAgentByEmailAddress = async (email: string): Promise<any | null> => {
828
+ const normalized = normalizeEmailAddress(email);
829
+ if (!normalized) return null;
830
+
831
+ for await (const [, value] of serverDb.iterator({
832
+ gte: "agent-",
833
+ lte: "agent-\uffff",
834
+ })) {
835
+ if (hasAgentEmailIdentity(value, normalized) && !value?.deletedAt) {
836
+ return value;
837
+ }
838
+ }
839
+ return null;
840
+ };
841
+
842
+ const findExistingInboundEmail = async (
843
+ ownerId: string,
844
+ messageId?: string,
845
+ providerMessageId?: string
846
+ ): Promise<EmailRecord | null> => {
847
+ if (!messageId && !providerMessageId) return null;
848
+ const { start, end } = emailKey.rangeOfOwner(ownerId);
849
+ for await (const [, value] of serverDb.iterator({ gte: start, lte: end })) {
850
+ if (!value || value.type !== DataType.EMAIL || value.deletedAt) continue;
851
+ const record = value as EmailRecord;
852
+ if (messageId && record.messageId === messageId) return record;
853
+ if (
854
+ providerMessageId &&
855
+ (record.meta as any)?.provider?.messageId === providerMessageId
856
+ ) {
857
+ return record;
858
+ }
859
+ }
860
+ return null;
861
+ };
862
+
863
+ export async function mirrorEmailToControlledInboxes(
864
+ params: MirrorEmailToControlledInboxesParams
865
+ ): Promise<EmailRecord[]> {
866
+ const recipients = normalizeParticipants([
867
+ ...(params.to || []),
868
+ ...(params.cc || []),
869
+ ...(params.bcc || []),
870
+ ]);
871
+ const mirrored: EmailRecord[] = [];
872
+ const mirroredOwners = new Set<string>();
873
+
874
+ for (const recipient of recipients) {
875
+ const agent = await findAgentByEmailAddress(recipient.email);
876
+ if (!agent?.dbKey || mirroredOwners.has(agent.dbKey)) continue;
877
+ mirroredOwners.add(agent.dbKey);
878
+
879
+ const existingRecord = await findExistingInboundEmail(
880
+ agent.dbKey,
881
+ undefined,
882
+ params.providerMessageId
883
+ );
884
+ if (existingRecord) {
885
+ mirrored.push(existingRecord);
886
+ continue;
887
+ }
888
+
889
+ const now = params.providerReceivedAt || new Date().toISOString();
890
+ const record = createEmailRecord({
891
+ ownerType: "agent",
892
+ ownerId: agent.dbKey,
893
+ tenantId: resolveKeyOwnerId(agent.dbKey) || agent.ownerId || agent.dbKey,
894
+ mailbox: "inbox",
895
+ status: "received",
896
+ from: params.from,
897
+ to: [recipient],
898
+ cc: params.cc,
899
+ bcc: params.bcc,
900
+ replyTo: params.replyTo,
901
+ subject: params.subject,
902
+ text: params.text,
903
+ html: params.html,
904
+ messageId: params.providerMessageId,
905
+ meta: {
906
+ ...(params.meta || {}),
907
+ provider: {
908
+ name: params.providerName,
909
+ messageId: params.providerMessageId,
910
+ mirrored: true,
911
+ receivedAt: now,
912
+ rawHeaders: params.rawHeaders,
913
+ raw: params.raw,
914
+ },
915
+ },
916
+ now,
917
+ });
918
+ await serverDb.put(record.dbKey, record);
919
+ mirrored.push(record);
920
+ }
921
+
922
+ return mirrored;
923
+ }
924
+
925
+ const allowedRecipientEmails = async (recipients: EmailParticipant[]) => {
926
+ const allowlist = new Set(
927
+ String(process.env.EMAIL_TEST_ALLOWLIST || "")
928
+ .split(",")
929
+ .map((item) => normalizeEmailAddress(item))
930
+ .filter(Boolean)
931
+ );
932
+
933
+ const denied: string[] = [];
934
+ for (const recipient of recipients) {
935
+ const email = normalizeEmailAddress(recipient.email);
936
+ if (allowlist.has(email)) continue;
937
+ if (await findAgentByEmailAddress(email)) continue;
938
+ denied.push(recipient.email);
939
+ }
940
+
941
+ return denied;
942
+ };
943
+
944
+ const providerMeta = (
945
+ provider: EmailProvider,
946
+ result: EmailProviderSendResult,
947
+ extra?: Record<string, unknown>
948
+ ) => ({
949
+ provider: {
950
+ name: provider.name,
951
+ messageId: result.providerMessageId,
952
+ delivered: result.delivered,
953
+ queued: result.queued,
954
+ permanentBounces: result.permanentBounces,
955
+ error: result.error,
956
+ raw: result.raw,
957
+ ...extra,
958
+ },
959
+ });
960
+
961
+ const ensureAgentEmailDelegation = async (
962
+ principalUserId: string,
963
+ agentId: string
964
+ ) => {
965
+ const key = agentDelegationKey(principalUserId, agentId);
966
+ const existing = await serverDb.get(key).catch(() => null);
967
+ const scopes = new Set(Array.isArray(existing?.scopes) ? existing.scopes : []);
968
+ ["email:read", "email:manage", "email:send"].forEach((scope) => scopes.add(scope));
969
+
970
+ const prefixes = new Set(
971
+ Array.isArray(existing?.resourcePrefixes) ? existing.resourcePrefixes : []
972
+ );
973
+ prefixes.add(`email-${agentId}-`);
974
+
975
+ await serverDb.put(key, {
976
+ ...existing,
977
+ principalUserId,
978
+ agentId,
979
+ scopes: Array.from(scopes),
980
+ resourcePrefixes: Array.from(prefixes),
981
+ });
982
+ };
983
+
984
+ export function createEmailRecord(input: CreateEmailRecordInput): EmailRecord {
985
+ if (!input.ownerId?.trim()) {
986
+ throw apiError("invalid_input", "ownerId is required");
987
+ }
988
+ if (!input.from?.email?.trim()) {
989
+ throw apiError("invalid_input", "from.email is required");
990
+ }
991
+
992
+ const ownerId = input.ownerId.trim();
993
+ const { dbKey, emailId } = emailKey.create(ownerId);
994
+ const now = input.now ?? new Date().toISOString();
995
+
996
+ return {
997
+ dbKey,
998
+ type: DataType.EMAIL,
999
+ ownerType: input.ownerType ?? "user",
1000
+ ownerId,
1001
+ tenantId: input.tenantId ?? ownerId,
1002
+ spaceId: input.spaceId ?? null,
1003
+ mailbox: input.mailbox ?? "inbox",
1004
+ status: input.status ?? "received",
1005
+ from: input.from,
1006
+ to: Array.isArray(input.to) ? input.to : [],
1007
+ cc: input.cc,
1008
+ bcc: input.bcc,
1009
+ replyTo: input.replyTo,
1010
+ subject: input.subject ?? "",
1011
+ text: input.text,
1012
+ html: input.html,
1013
+ messageId: input.messageId ?? emailId,
1014
+ threadId: input.threadId,
1015
+ inReplyTo: input.inReplyTo,
1016
+ references: input.references,
1017
+ tags: normalizeTags(input.tags),
1018
+ meta: input.meta ?? {},
1019
+ createdAt: now,
1020
+ updatedAt: now,
1021
+ };
1022
+ }
1023
+
1024
+ export async function listEmails(
1025
+ params: ListEmailsParams = {},
1026
+ context?: ApiContext
1027
+ ): Promise<EmailRecord[]> {
1028
+ const actor = requireActor(context);
1029
+ const ownerId = params.ownerId?.trim() || actorOwnerId(actor);
1030
+ if (!ownerId) throw apiError("forbidden", "Email owner is required");
1031
+
1032
+ const actorOwner = actorOwnerId(actor);
1033
+ const isOwnedAgentMailbox =
1034
+ actorOwner && resolveKeyOwnerId(ownerId) === actorOwner;
1035
+ if (ownerId !== actorOwner && !isOwnedAgentMailbox) {
1036
+ throw apiError("forbidden", "Cannot list emails for another owner");
1037
+ }
1038
+
1039
+ const { start, end } = emailKey.rangeOfOwner(ownerId);
1040
+ const limit = Math.min(
1041
+ Math.max(Number(params.limit ?? 50), 1),
1042
+ MAX_LIST_LIMIT
1043
+ );
1044
+ const rows: EmailRecord[] = [];
1045
+
1046
+ for await (const [, value] of serverDb.iterator({ gte: start, lte: end })) {
1047
+ if (!value || value.type !== DataType.EMAIL || value.deletedAt) continue;
1048
+ const record = value as EmailRecord;
1049
+ if (params.mailbox && record.mailbox !== params.mailbox) continue;
1050
+ if (params.status && record.status !== params.status) continue;
1051
+ if (params.tag && !record.tags?.includes(params.tag)) continue;
1052
+
1053
+ const access = await authorizeActorRecordAccess({
1054
+ action: "read",
1055
+ actor,
1056
+ dbKey: record.dbKey,
1057
+ record,
1058
+ });
1059
+ if (!access.allowed) continue;
1060
+
1061
+ rows.push(record);
1062
+ if (rows.length >= limit) break;
1063
+ }
1064
+
1065
+ return rows.sort((a, b) => {
1066
+ const bTime = new Date(b.updatedAt).getTime();
1067
+ const aTime = new Date(a.updatedAt).getTime();
1068
+ return (Number.isFinite(bTime) ? bTime : 0) - (Number.isFinite(aTime) ? aTime : 0);
1069
+ });
1070
+ }
1071
+
1072
+ export async function getEmail(
1073
+ params: EmailKeyParams,
1074
+ context?: ApiContext
1075
+ ): Promise<EmailRecord> {
1076
+ const actor = requireActor(context);
1077
+ const record = await loadEmailRecord(params?.dbKey);
1078
+
1079
+ await assertEmailAccess(actor, "read", record);
1080
+ return record;
1081
+ }
1082
+
1083
+ export async function updateEmailTags(
1084
+ params: UpdateEmailTagsParams,
1085
+ context?: ApiContext
1086
+ ): Promise<EmailRecord> {
1087
+ const actor = requireActor(context);
1088
+ const record = await loadEmailRecord(params?.dbKey);
1089
+ await assertEmailAccess(actor, "manage", record);
1090
+
1091
+ const updated: EmailRecord = {
1092
+ ...record,
1093
+ tags: normalizeTags(params.tags),
1094
+ updatedAt: new Date().toISOString(),
1095
+ };
1096
+ await serverDb.put(record.dbKey, updated);
1097
+ return updated;
1098
+ }
1099
+
1100
+ export async function archiveEmail(
1101
+ params: EmailKeyParams,
1102
+ context?: ApiContext
1103
+ ): Promise<EmailRecord> {
1104
+ const actor = requireActor(context);
1105
+ const record = await loadEmailRecord(params?.dbKey);
1106
+ await assertEmailAccess(actor, "manage", record);
1107
+
1108
+ const updated: EmailRecord = {
1109
+ ...record,
1110
+ mailbox: "archive",
1111
+ updatedAt: new Date().toISOString(),
1112
+ };
1113
+ await serverDb.put(record.dbKey, updated);
1114
+ return updated;
1115
+ }
1116
+
1117
+ export async function sendEmail(
1118
+ params: SendEmailParams,
1119
+ context?: ApiContext,
1120
+ provider: EmailProvider = getEmailProvider()
1121
+ ): Promise<EmailRecord> {
1122
+ if (!context?.userId) {
1123
+ throw apiError("unauthorized", "Authentication required");
1124
+ }
1125
+
1126
+ const agentId = params?.agentId?.trim() || "";
1127
+ const agent = await loadOwnedAgent(agentId, context.userId);
1128
+ const fromEmail = normalizeEmailAddress(params?.from?.email);
1129
+ if (getAgentEmailIdentities(agent).length === 0) {
1130
+ throw apiError("invalid_input", "Agent does not have an email identity");
1131
+ }
1132
+ if (!fromEmail || !hasAgentEmailIdentity(agent, fromEmail)) {
1133
+ throw apiError("forbidden", "from must match one of the agent email identities");
1134
+ }
1135
+
1136
+ const to = normalizeParticipants(params?.to);
1137
+ if (to.length === 0) {
1138
+ throw apiError("invalid_input", "to is required");
1139
+ }
1140
+ if (!params.subject?.trim()) {
1141
+ throw apiError("invalid_input", "subject is required");
1142
+ }
1143
+ if (!params.text?.trim() && !params.html?.trim()) {
1144
+ throw apiError("invalid_input", "text or html is required");
1145
+ }
1146
+
1147
+ const deniedRecipients = await allowedRecipientEmails(to);
1148
+ if (deniedRecipients.length > 0) {
1149
+ throw apiError("forbidden", "Recipient is not an allowed agent or test address");
1150
+ }
1151
+
1152
+ const now = new Date().toISOString();
1153
+ const queued = createEmailRecord({
1154
+ ownerType: "agent",
1155
+ ownerId: agentId,
1156
+ tenantId: context.userId,
1157
+ mailbox: "sent",
1158
+ status: "queued",
1159
+ from: { ...params.from, email: fromEmail },
1160
+ to,
1161
+ cc: normalizeParticipants(params.cc),
1162
+ bcc: normalizeParticipants(params.bcc),
1163
+ replyTo: normalizeParticipants(params.replyTo),
1164
+ subject: params.subject,
1165
+ text: params.text,
1166
+ html: params.html,
1167
+ meta: {
1168
+ agentId,
1169
+ requestedByUserId: context.userId,
1170
+ queuedAt: now,
1171
+ },
1172
+ now,
1173
+ });
1174
+
1175
+ await assertEmailAccess(
1176
+ { type: "agent", agentId, principalUserId: context.userId },
1177
+ "write",
1178
+ queued
1179
+ );
1180
+ await serverDb.put(queued.dbKey, queued);
1181
+
1182
+ try {
1183
+ const result = await provider.sendEmail({
1184
+ from: queued.from,
1185
+ to: queued.to,
1186
+ cc: queued.cc,
1187
+ bcc: queued.bcc,
1188
+ replyTo: queued.replyTo,
1189
+ subject: queued.subject,
1190
+ text: queued.text,
1191
+ html: queued.html,
1192
+ });
1193
+ const status: EmailStatus = result.status === "failed" ? "failed" : "sent";
1194
+ const updated: EmailRecord = {
1195
+ ...queued,
1196
+ status,
1197
+ updatedAt: new Date().toISOString(),
1198
+ meta: {
1199
+ ...queued.meta,
1200
+ ...providerMeta(provider, result),
1201
+ },
1202
+ };
1203
+ await serverDb.put(updated.dbKey, updated);
1204
+ if (status !== "failed") {
1205
+ await mirrorEmailToControlledInboxes({
1206
+ from: queued.from,
1207
+ to: queued.to,
1208
+ cc: queued.cc,
1209
+ bcc: queued.bcc,
1210
+ replyTo: queued.replyTo,
1211
+ subject: queued.subject,
1212
+ text: queued.text,
1213
+ html: queued.html,
1214
+ providerName: provider.name,
1215
+ providerMessageId: result.providerMessageId,
1216
+ meta: {
1217
+ source: "sendEmail",
1218
+ sentRecordKey: updated.dbKey,
1219
+ },
1220
+ });
1221
+ }
1222
+ return updated;
1223
+ } catch (error: any) {
1224
+ const failed: EmailRecord = {
1225
+ ...queued,
1226
+ status: "failed",
1227
+ updatedAt: new Date().toISOString(),
1228
+ meta: {
1229
+ ...queued.meta,
1230
+ provider: {
1231
+ name: provider.name,
1232
+ error: error?.message || String(error),
1233
+ },
1234
+ },
1235
+ };
1236
+ await serverDb.put(failed.dbKey, failed);
1237
+ return failed;
1238
+ }
1239
+ }
1240
+
1241
+ export async function receiveInboundEmail(
1242
+ payload: unknown,
1243
+ provider: EmailProvider = getEmailProvider()
1244
+ ): Promise<ReceiveInboundEmailResult> {
1245
+ const inbound = await provider.parseInbound(payload);
1246
+ const records: EmailRecord[] = [];
1247
+ const existing: EmailRecord[] = [];
1248
+ const unresolved: string[] = [];
1249
+
1250
+ for (const recipient of inbound.to) {
1251
+ const agent = await findAgentByEmailAddress(recipient.email);
1252
+ if (!agent?.dbKey) {
1253
+ unresolved.push(recipient.email);
1254
+ continue;
1255
+ }
1256
+
1257
+ const existingRecord = await findExistingInboundEmail(
1258
+ agent.dbKey,
1259
+ inbound.messageId,
1260
+ inbound.providerMessageId
1261
+ );
1262
+ if (existingRecord) {
1263
+ existing.push(existingRecord);
1264
+ continue;
1265
+ }
1266
+
1267
+ const now = inbound.providerReceivedAt || new Date().toISOString();
1268
+ const record = createEmailRecord({
1269
+ ownerType: "agent",
1270
+ ownerId: agent.dbKey,
1271
+ tenantId: resolveKeyOwnerId(agent.dbKey) || agent.ownerId || agent.dbKey,
1272
+ mailbox: "inbox",
1273
+ status: "received",
1274
+ from: inbound.from,
1275
+ to: [recipient],
1276
+ cc: inbound.cc,
1277
+ bcc: inbound.bcc,
1278
+ replyTo: inbound.replyTo,
1279
+ subject: inbound.subject,
1280
+ text: inbound.text,
1281
+ html: inbound.html,
1282
+ messageId: inbound.messageId,
1283
+ meta: {
1284
+ provider: {
1285
+ name: provider.name,
1286
+ messageId: inbound.providerMessageId,
1287
+ receivedAt: now,
1288
+ rawHeaders: inbound.rawHeaders,
1289
+ raw: inbound.raw,
1290
+ },
1291
+ },
1292
+ now,
1293
+ });
1294
+ await serverDb.put(record.dbKey, record);
1295
+ records.push(record);
1296
+ }
1297
+
1298
+ if (unresolved.length > 0 && records.length === 0 && existing.length === 0) {
1299
+ throw apiError("not_found", `No agent email identity found for ${unresolved.join(", ")}`);
1300
+ }
1301
+
1302
+ return { records, existing };
1303
+ }
1304
+
1305
+ export async function bindAgentEmailIdentity(
1306
+ params: BindAgentEmailIdentityParams,
1307
+ context?: ApiContext
1308
+ ) {
1309
+ if (!context?.userId) {
1310
+ throw apiError("unauthorized", "Authentication required");
1311
+ }
1312
+
1313
+ const agentId = params?.agentId?.trim() || "";
1314
+ const emailAddress = normalizeEmailAddress(params?.emailAddress);
1315
+ assertValidEmailAddress(emailAddress);
1316
+
1317
+ const agent = await loadOwnedAgent(agentId, context.userId);
1318
+ const existing = await findAgentByEmailAddress(emailAddress);
1319
+ if (existing?.dbKey && existing.dbKey !== agentId) {
1320
+ throw apiError("invalid_input", "emailAddress is already bound to another agent");
1321
+ }
1322
+
1323
+ const now = new Date().toISOString();
1324
+ const updated = upsertAgentEmailIdentity(
1325
+ agent,
1326
+ {
1327
+ emailAddress,
1328
+ provider: params.provider || agent.meta?.emailProvider || process.env.EMAIL_PROVIDER || "mock",
1329
+ createdAt: now,
1330
+ verifiedAt: now,
1331
+ readinessStatus: "warming",
1332
+ ingressReadyAt: null,
1333
+ lastWarmupAt: now,
1334
+ lastWarmupError: null,
1335
+ source: "bound",
1336
+ },
1337
+ { makePrimary: true }
1338
+ );
1339
+
1340
+ await ensureCloudflareEmailRoutingRule(emailAddress, params.provider);
1341
+ await serverDb.put(agentId, updated);
1342
+ try {
1343
+ const ingressReadyAt = await waitForCloudflareRouteReadiness(
1344
+ agentId,
1345
+ emailAddress,
1346
+ extractDomainFromEmailValue(emailAddress),
1347
+ params.provider || updated.meta?.emailProvider
1348
+ );
1349
+ const readyAgent = upsertAgentEmailIdentity(
1350
+ updated,
1351
+ withReadinessState(getAgentEmailIdentity(updated, emailAddress) || { emailAddress }, {
1352
+ readinessStatus: "ready",
1353
+ ingressReadyAt,
1354
+ lastWarmupAt: new Date().toISOString(),
1355
+ lastWarmupError: null,
1356
+ }),
1357
+ { makePrimary: true }
1358
+ );
1359
+ await serverDb.put(agentId, readyAgent);
1360
+ await ensureAgentEmailDelegation(context.userId, agentId);
1361
+
1362
+ return buildBindIdentityResult(agentId, emailAddress, readyAgent);
1363
+ } catch (error: any) {
1364
+ const failedWarmupAgent = upsertAgentEmailIdentity(
1365
+ updated,
1366
+ withReadinessState(getAgentEmailIdentity(updated, emailAddress) || { emailAddress }, {
1367
+ readinessStatus: "failed_warmup",
1368
+ ingressReadyAt: null,
1369
+ lastWarmupAt: new Date().toISOString(),
1370
+ lastWarmupError: error?.message || String(error),
1371
+ }),
1372
+ { makePrimary: true }
1373
+ );
1374
+ await serverDb.put(agentId, failedWarmupAgent);
1375
+ await ensureAgentEmailDelegation(context.userId, agentId);
1376
+ return buildBindIdentityResult(agentId, emailAddress, failedWarmupAgent);
1377
+ }
1378
+ }
1379
+
1380
+ export async function provisionAgentEmailIdentity(
1381
+ params: ProvisionAgentEmailIdentityParams,
1382
+ context?: ApiContext
1383
+ ) {
1384
+ if (!context?.userId) {
1385
+ throw apiError("unauthorized", "Authentication required");
1386
+ }
1387
+
1388
+ const agentId = params?.agentId?.trim() || "";
1389
+ const agent = await loadOwnedAgent(agentId, context.userId);
1390
+ const domain = resolveAgentEmailDomain(params?.domain);
1391
+ const localPart = normalizeLocalPart(params?.localPart) ||
1392
+ buildProvisionedLocalPart(agentId, params?.purpose);
1393
+ assertValidLocalPart(localPart);
1394
+
1395
+ const emailAddress = `${localPart}@${domain}`;
1396
+ assertValidEmailAddress(emailAddress);
1397
+
1398
+ const existing = await findAgentByEmailAddress(emailAddress);
1399
+ if (existing?.dbKey && existing.dbKey !== agentId) {
1400
+ throw apiError("invalid_input", "emailAddress is already bound to another agent");
1401
+ }
1402
+
1403
+ const now = new Date().toISOString();
1404
+ const provider =
1405
+ params?.provider ||
1406
+ agent.meta?.emailProvider ||
1407
+ process.env.EMAIL_PROVIDER ||
1408
+ DEFAULT_AGENT_EMAIL_PROVIDER;
1409
+ const updated = upsertAgentEmailIdentity(
1410
+ agent,
1411
+ {
1412
+ emailAddress,
1413
+ provider,
1414
+ purpose: normalizePurpose(params?.purpose) || undefined,
1415
+ createdAt: now,
1416
+ verifiedAt: now,
1417
+ readinessStatus: "warming",
1418
+ ingressReadyAt: null,
1419
+ lastWarmupAt: now,
1420
+ lastWarmupError: null,
1421
+ source: "provisioned",
1422
+ },
1423
+ { makePrimary: params?.makePrimary }
1424
+ );
1425
+
1426
+ await ensureCloudflareEmailRoutingRule(emailAddress, provider);
1427
+ await serverDb.put(agentId, updated);
1428
+ try {
1429
+ const ingressReadyAt = await waitForCloudflareRouteReadiness(
1430
+ agentId,
1431
+ emailAddress,
1432
+ domain,
1433
+ provider
1434
+ );
1435
+ const readyAgent = upsertAgentEmailIdentity(
1436
+ updated,
1437
+ withReadinessState(getAgentEmailIdentity(updated, emailAddress) || { emailAddress }, {
1438
+ readinessStatus: "ready",
1439
+ ingressReadyAt,
1440
+ lastWarmupAt: new Date().toISOString(),
1441
+ lastWarmupError: null,
1442
+ }),
1443
+ { makePrimary: params?.makePrimary }
1444
+ );
1445
+ await serverDb.put(agentId, readyAgent);
1446
+ await ensureAgentEmailDelegation(context.userId, agentId);
1447
+
1448
+ return buildProvisionIdentityResult(
1449
+ agentId,
1450
+ emailAddress,
1451
+ localPart,
1452
+ domain,
1453
+ provider,
1454
+ normalizePurpose(params?.purpose) || null,
1455
+ readyAgent
1456
+ );
1457
+ } catch (error: any) {
1458
+ const failedWarmupAgent = upsertAgentEmailIdentity(
1459
+ updated,
1460
+ withReadinessState(getAgentEmailIdentity(updated, emailAddress) || { emailAddress }, {
1461
+ readinessStatus: "failed_warmup",
1462
+ ingressReadyAt: null,
1463
+ lastWarmupAt: new Date().toISOString(),
1464
+ lastWarmupError: error?.message || String(error),
1465
+ }),
1466
+ { makePrimary: params?.makePrimary }
1467
+ );
1468
+ await serverDb.put(agentId, failedWarmupAgent);
1469
+ await ensureAgentEmailDelegation(context.userId, agentId);
1470
+ return buildProvisionIdentityResult(
1471
+ agentId,
1472
+ emailAddress,
1473
+ localPart,
1474
+ domain,
1475
+ provider,
1476
+ normalizePurpose(params?.purpose) || null,
1477
+ failedWarmupAgent
1478
+ );
1479
+ }
1480
+ }