@frockbot/cloudflare 0.0.0 → 0.7.291

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 (88) hide show
  1. package/build-artifact.ts +130 -0
  2. package/build-flutter-web.ts +508 -0
  3. package/deployment-config/README.md +414 -0
  4. package/deployment-config/cli.ts +228 -0
  5. package/deployment-config/generate.ts +689 -0
  6. package/deployment-config/jsonc.ts +18 -0
  7. package/deployment-config/profile-schema.generated.ts +391 -0
  8. package/deployment-config/profile.schema.json +358 -0
  9. package/deployment-config/profile.ts +124 -0
  10. package/migrations/0001_better_auth.sql +15 -0
  11. package/migrations/0002_drop_account_issuer.sql +8 -0
  12. package/package.json +61 -5
  13. package/release-version.ts +20 -0
  14. package/src/account-admission.ts +179 -0
  15. package/src/account-deletion.ts +274 -0
  16. package/src/admin-entrypoint.ts +194 -0
  17. package/src/admin-identities.ts +26 -0
  18. package/src/audit.ts +103 -0
  19. package/src/auth-package.access.ts +24 -0
  20. package/src/auth-package.ts +36 -0
  21. package/src/avatar-state-cleanup.ts +107 -0
  22. package/src/billing-computer.ts +267 -0
  23. package/src/billing-readiness.ts +21 -0
  24. package/src/billing.ts +461 -0
  25. package/src/bot-capabilities.ts +480 -0
  26. package/src/bot-recovery.ts +1 -0
  27. package/src/bot-state-channel.ts +702 -0
  28. package/src/bot-state.ts +3995 -0
  29. package/src/bot-template-cleanup.ts +98 -0
  30. package/src/bot-title-cleanup.ts +104 -0
  31. package/src/brand-icon.png +0 -0
  32. package/src/brand-logo.ts +2 -0
  33. package/src/brand.ts +36 -0
  34. package/src/client-compatibility.ts +51 -0
  35. package/src/compaction-announcement-cleanup.ts +113 -0
  36. package/src/computer-egress.ts +110 -0
  37. package/src/computer-host.ts +92 -0
  38. package/src/computer-screenshot-cleanup.ts +78 -0
  39. package/src/contracts.ts +832 -0
  40. package/src/debug.ts +272 -0
  41. package/src/default-packages-marker-cleanup.ts +39 -0
  42. package/src/deployment-policy-admin-host.ts +121 -0
  43. package/src/deployment-policy.ts +426 -0
  44. package/src/directory-profile-cleanup.ts +101 -0
  45. package/src/durable-rpc.ts +753 -0
  46. package/src/durable-session.ts +21 -0
  47. package/src/entry-boundary.ts +103 -0
  48. package/src/frock-ai.ts +338 -0
  49. package/src/gateway.ts +1392 -0
  50. package/src/group-chat.ts +736 -0
  51. package/src/hidden-bot-notifications-cleanup.ts +32 -0
  52. package/src/index.ts +2798 -0
  53. package/src/insights.ts +15 -0
  54. package/src/machine-messages-cleanup.ts +121 -0
  55. package/src/machine-socket.ts +164 -0
  56. package/src/memory-records.ts +181 -0
  57. package/src/memory.ts +145 -0
  58. package/src/model-rates.ts +252 -0
  59. package/src/native-auth.ts +1006 -0
  60. package/src/native-sessions.ts +121 -0
  61. package/src/notification-state-cleanup.ts +18 -0
  62. package/src/ollama-web-search-cleanup.ts +69 -0
  63. package/src/package-page-shapes-cleanup.ts +190 -0
  64. package/src/plugin-egress.ts +31 -0
  65. package/src/plugin-page-route.ts +72 -0
  66. package/src/plugin-panels-cleanup.ts +101 -0
  67. package/src/prepared-input-cleanup.ts +105 -0
  68. package/src/production-secrets.ts +484 -0
  69. package/src/project-cleanup.ts +123 -0
  70. package/src/project-events-cleanup.ts +152 -0
  71. package/src/publication-state-cleanup.ts +92 -0
  72. package/src/push.ts +498 -0
  73. package/src/request-body.ts +165 -0
  74. package/src/routine-state-cleanup.ts +57 -0
  75. package/src/search.ts +88 -0
  76. package/src/sidebar-label-cleanup.ts +165 -0
  77. package/src/skill-index-cleanup.ts +53 -0
  78. package/src/supersede-cleanup.ts +162 -0
  79. package/src/test-chat-cleanup.ts +93 -0
  80. package/src/uploads.ts +486 -0
  81. package/src/user-application.ts +1068 -0
  82. package/src/user-configuration.ts +4491 -0
  83. package/src/voice-assistant.ts +4999 -0
  84. package/src/voice-dictation.ts +743 -0
  85. package/src/working-context-cleanup.ts +96 -0
  86. package/src/workspace.ts +201 -0
  87. package/wrangler.jsonc +385 -0
  88. package/README.md +0 -3
package/src/index.ts ADDED
@@ -0,0 +1,2798 @@
1
+ import { billingRoutes, type BillingAccountRpc } from "./billing.js";
2
+ import { decodeHostedModelRatesV1 } from "@frockbot/app/billing/rates";
3
+ import { WorkerEntrypoint } from "cloudflare:workers";
4
+ import { BOT_STATE_CHANNEL_INTERNAL_PATH } from "./bot-state-channel.js";
5
+ import { internalMachineSocketRequestV1 } from "./machine-socket.js";
6
+ import {
7
+ decodeMachineResultDeliveryV1,
8
+ type MachineResultDeliveryV1,
9
+ } from "@frockbot/app/machine/delivery";
10
+ import type {
11
+ AuthIdentityCandidateV1,
12
+ AuthPackageIdentityStoreV1,
13
+ } from "@frockbot/core/contracts";
14
+ import { pluginModuleKeyV1, pluginPageKeyV1 } from "@frockbot/core/contracts";
15
+ import type { ClientSkillCatalogV1 } from "@frockbot/app/shell/skill-protocol";
16
+ import { GroupChat, GROUP_CHANNEL_INTERNAL_PATH } from "./group-chat.js";
17
+ import {
18
+ groupChatObjectNameV1,
19
+ unwrapGroupRpcV1,
20
+ type GroupChatCommandV1,
21
+ type GroupChatListV1,
22
+ type GroupChatReceiptV1,
23
+ type GroupChatViewV1,
24
+ type GroupMessagePageV1,
25
+ type GroupMessageV1,
26
+ type GroupPostCommandV1,
27
+ type GroupReadCommandV1,
28
+ type GroupRetryCommandV1,
29
+ type GroupStopCommandV1,
30
+ } from "@frockbot/app/groups/shared";
31
+ import { createFoundationBackendContributions } from "@frockbot/app/runtime";
32
+ import {
33
+ decodeBotLifecycleDirectoryViewV1,
34
+ decodeBotLifecycleReceiptV1,
35
+ type BotLifecycleCommandV1,
36
+ decodeBotMembershipViewV1,
37
+ decodeDirectoryViewV1,
38
+ decodeFlockBootstrapViewV1,
39
+ type FlockBootstrapViewV1,
40
+ decodeFlockReceiptV1,
41
+ decodeAvatarIdentityViewV1,
42
+ decodeVoiceIdentityViewV1,
43
+ decodeLookIdentityViewV1,
44
+ BotNotFoundError,
45
+ decodeBotIdentityDirectoryViewV1,
46
+ FLOCK_DIRECTORY_LIMIT,
47
+ type BotIdentityDirectoryViewV1,
48
+ type BotIdentityViewV1,
49
+ } from "@frockbot/app/flock/shared";
50
+ import {
51
+ decodeClientRunListQueryV1,
52
+ decodeClientRunLookupQueryV1,
53
+ decodeClientRunStopCommandV1,
54
+ type ClientRunLookupQueryV1,
55
+ type ClientRunLookupV1,
56
+ type ClientRunQuestionsV1,
57
+ type ClientRunListQueryV1,
58
+ type ClientRunListV1,
59
+ type ClientRunStopCommandV1,
60
+ type ClientRunStopReceiptV1,
61
+ } from "@frockbot/app/shell/run-protocol";
62
+ import {
63
+ decodeBotNotificationDirectoryViewV1,
64
+ decodeBotUnreadDirectoryViewV1,
65
+ decodeBotUnreadReceiptV1,
66
+ type BotNotificationDirectoryViewV1,
67
+ type BotUnreadCommandV1,
68
+ type BotUnreadDirectoryViewV1,
69
+ type BotUnreadReceiptV1,
70
+ type BotUnreadViewV1,
71
+ } from "@frockbot/app/shell/unread";
72
+ import {
73
+ decodeApprovalDecisionCommandV1,
74
+ decodeApprovalDecisionReceiptV1,
75
+ decodeApprovalListViewV1,
76
+ type ApprovalDecisionCommandV1,
77
+ type ApprovalDecisionReceiptV1,
78
+ type ApprovalListViewV1,
79
+ } from "@frockbot/app/shell/approvals";
80
+ import {
81
+ decodeCardActionCommandV1,
82
+ decodeCardActionReceiptV1,
83
+ decodeCardListViewV1,
84
+ decodeCardSurfaceIdV1,
85
+ decodeCardViewV1,
86
+ type CardActionCommandV1,
87
+ type CardActionReceiptV1,
88
+ type CardListViewV1,
89
+ type CardViewV1,
90
+ } from "@frockbot/app/shell/cards";
91
+ import {
92
+ decodeSecretListViewV1,
93
+ decodeSecretSubmitCommandV1,
94
+ decodeSecretSubmitReceiptV1,
95
+ type SecretSubmitCommandV1,
96
+ type SecretSubmitReceiptV1,
97
+ } from "@frockbot/app/secrets/shared";
98
+ import {
99
+ decodeCompositionCommandReceiptV1,
100
+ decodeCompositionGenerationListViewV1,
101
+ decodeCompositionGenerationViewV1,
102
+ decodeBotIdV1,
103
+ type BotSettingsViewV1,
104
+ } from "@frockbot/core/configuration";
105
+ import {
106
+ decodeRoutineCommandReceiptV1,
107
+ decodeRoutineInboxReceiptV1,
108
+ decodeRoutineInboxViewV1,
109
+ decodeRoutineListViewV1,
110
+ decodeRoutineRunDetailViewV1,
111
+ decodeRoutineRunListViewV1,
112
+ } from "@frockbot/app/routines/shared";
113
+ import {
114
+ decodeComputerCommandResponse,
115
+ decodeComputerProjectionV1,
116
+ type ComputerCommandV1,
117
+ } from "@frockbot/computer/protocol";
118
+ import { ComputerBotNotFoundError } from "@frockbot/computer/backend";
119
+ import {
120
+ decodeTaskListViewV1,
121
+ decodeTaskViewV1,
122
+ } from "@frockbot/app/subagents/shared";
123
+ import {
124
+ decodeMachineClaimReceiptV1,
125
+ MachineTokenError,
126
+ decodeMachineEnrollmentReceiptV1,
127
+ decodeMachineListViewV1,
128
+ decodeMachineModuleCallClaimReceiptV1,
129
+ decodeMachineModuleCallResultReceiptV1,
130
+ decodeMachineModuleEventsReceiptV1,
131
+ decodeMachineModuleReportsReceiptV1,
132
+ decodeMachinePairingOfferV1,
133
+ decodeMachineResultReceiptV1,
134
+ } from "@frockbot/core/machine-protocol";
135
+ import {
136
+ decodeClientSearchRebuildReceiptV1,
137
+ decodeSearchIndexResultsV1,
138
+ type SearchQueryV1,
139
+ } from "@frockbot/app/search";
140
+ import {
141
+ decodeAuditActivityPageV1,
142
+ decodeAuditRebuildReceiptV1,
143
+ decodeClientAuditPageV1,
144
+ type AuditActivityQueryV1,
145
+ type AuditQueryV1,
146
+ } from "@frockbot/app/audit";
147
+ import {
148
+ accessEmailV1,
149
+ admissionRefusalCopyV1,
150
+ decodeAccountAdmissionDecisionV1,
151
+ type AccountAdmissionDecisionV1,
152
+ type AdmissionIdentityV1,
153
+ decodeUserFeaturesV1,
154
+ } from "@frockbot/app/admin/shared";
155
+ import { AUTH_PACKAGE_V1 } from "#auth-package";
156
+ import { BRAND_V1 } from "#brand";
157
+ import {
158
+ createNativeAuth,
159
+ nativeAppsV1,
160
+ nativeDevelopmentReturnUriV1,
161
+ nativeReturnUris,
162
+ } from "./native-auth.js";
163
+ import {
164
+ DEVELOPMENT_USER_ID,
165
+ isDeploymentAdminV1,
166
+ } from "./admin-identities.js";
167
+ import type { DebugGatewaySurface } from "./debug.js";
168
+ import type { BotDebugQueryV1 } from "@frockbot/app/shell/debug-protocol";
169
+ import {
170
+ BotState,
171
+ frockAiWorkerVarV1,
172
+ type OwnedBotTurnCommand,
173
+ } from "./bot-state.js";
174
+ import type {
175
+ ApplicationArtifactStore,
176
+ BotConfigurationBinding,
177
+ BotNotificationIntent,
178
+ BotTurnCommand,
179
+ BotTurnResult,
180
+ BotPackageLoader,
181
+ UserConfigurationBinding,
182
+ WorkerLoader,
183
+ ClientWorkspaceFileV1,
184
+ } from "./contracts.js";
185
+ import { createGateway } from "./gateway.js";
186
+ import { getAgentByName } from "agents";
187
+ import {
188
+ VOICE_ASSISTANT_DEVICE_HEADER,
189
+ VOICE_ASSISTANT_INTERNAL_PATH,
190
+ VOICE_ASSISTANT_USER_HEADER,
191
+ VoiceAssistant,
192
+ voiceAssistantConfiguredV1,
193
+ } from "./voice-assistant.js";
194
+ import {
195
+ openVoiceDictationRelayV1,
196
+ type VoiceDictationCleanupV1,
197
+ } from "./voice-dictation.js";
198
+ import { createFrockAiGatewayHostV1 } from "./frock-ai.js";
199
+ import {
200
+ storeUploadV1,
201
+ uploadRoutes,
202
+ type UploadRouteDependenciesV1,
203
+ } from "./uploads.js";
204
+ import {
205
+ decodeInboundEmailRouteDecisionV1,
206
+ decodeInboundEmailStateV1,
207
+ emailDomainV1,
208
+ normalizeSenderAddressV1,
209
+ } from "@frockbot/app/email/shared";
210
+ import {
211
+ receiveInboundEmailV1,
212
+ type InboundEmailHostV1,
213
+ } from "@frockbot/app/email/inbound";
214
+ import type { DocumentConverterV1 } from "@frockbot/app/uploads/extract";
215
+ import { createHostedDictationCleanupJudgeV1 } from "@frockbot/app/supervision";
216
+ import { VOICE_DICTATION_CLEANUP_MODEL_V1 } from "@frockbot/app/voice/dictation-cleanup";
217
+ import { voiceDictationConfiguredV1 } from "@frockbot/app/voice/dictation-upstream";
218
+ import { voiceAssistantEdgeTimingOfV1 } from "@frockbot/app/voice/diagnostics";
219
+ import {
220
+ COMPUTER_EGRESS_PATH_V1,
221
+ routeComputerEgressV1,
222
+ } from "./computer-egress.js";
223
+ import type { VoiceGatewayDependencies } from "./contracts.js";
224
+ import {
225
+ decodeRpcEnvelopeV1,
226
+ rpcBotId,
227
+ rpcDecoded,
228
+ rpcDecodedValue,
229
+ rpcIdentifier,
230
+ rpcJsonSnapshotV1,
231
+ rpcObject,
232
+ rpcBotTurnCommandOptionalsV1,
233
+ requireTurnCommandContentV1,
234
+ rpcTurnText,
235
+ type BotTurnCommandRequestV1,
236
+ rpcPattern,
237
+ rpcString,
238
+ } from "./durable-rpc.js";
239
+ import { UserConfiguration } from "./user-configuration.js";
240
+ import {
241
+ DEPLOYMENT_POLICY_SINGLETON_NAME,
242
+ DeploymentPolicy,
243
+ } from "./deployment-policy.js";
244
+ import { ACCOUNT_ADMISSION_UNAVAILABLE_MESSAGE } from "./account-admission.js";
245
+ import { RoutineHookError } from "@frockbot/app/routines/hook";
246
+ import type { ConnectTriggerOfferV1 } from "@frockbot/app/connect/triggers";
247
+ import type { ConnectEventV1 } from "@frockbot/app/connect/events";
248
+
249
+ export { BotCapabilities } from "./bot-capabilities.js";
250
+ export { PluginEgress } from "./plugin-egress.js";
251
+ export { BotState, DeploymentPolicy, UserConfiguration };
252
+ // One object per Group Chat, `idFromName("<userId>:<groupId>")`.
253
+ export { GroupChat };
254
+ // Administration, reached only by the admin portal over a service binding
255
+ // (ADR 0028). No route in this Worker answers for it.
256
+ export { AdminEntrypoint } from "./admin-entrypoint.js";
257
+ // The account-wide voice session (docs/voice.md): the one Agents SDK object.
258
+ export { VoiceAssistant };
259
+
260
+ interface Env {
261
+ STRIPE_SECRET_KEY?: string;
262
+ STRIPE_WEBHOOK_SECRET?: string;
263
+ STRIPE_MONTHLY_PRICE_ID?: string;
264
+ FCM_SERVICE_ACCOUNT?: string;
265
+ /** Explicit qualification gate; not enabled by the production configuration. */
266
+ NATIVE_SLICE_2_AUTH?: string;
267
+ /** The profile's `nativeApps`, as JSON: what the association files name. */
268
+ NATIVE_APPS?: string;
269
+ USER_APPLICATIONS: WorkerLoader;
270
+ // Bot-authored Package isolates, driven from the Bot Durable Object with
271
+ // `globalOutbound` disabled (plan Step 4). A separate loader namespace from
272
+ // USER_APPLICATIONS so the two never share an identity.
273
+ BOT_PACKAGES: BotPackageLoader;
274
+ APPLICATION_ARTIFACTS: R2Bucket;
275
+ MEMORY_FILES: R2Bucket;
276
+ MEMORY_INDEX: VectorizeIndex;
277
+ AI: Ai;
278
+ FROCK_AI_GATEWAY_ID?: string;
279
+ FROCK_AI_AUTO_ROUTE?: string;
280
+ /** Cloudflare account owning the AI Gateway; enables the compat transport. */
281
+ FROCK_AI_ACCOUNT_ID?: string;
282
+ /** `cf-aig-authorization` bearer for the authenticated AI Gateway. */
283
+ FROCK_AI_GATEWAY_TOKEN?: string;
284
+ // The pre-rename names. Each `FROCK_AI_*` setting falls back to its
285
+ // `FLOCK_AI_*` twin so a deployment whose vars and secrets still carry the
286
+ // old names keeps working; drop these once every environment is renamed.
287
+ FLOCK_AI_GATEWAY_ID?: string;
288
+ FLOCK_AI_AUTO_ROUTE?: string;
289
+ FLOCK_AI_ACCOUNT_ID?: string;
290
+ FLOCK_AI_GATEWAY_TOKEN?: string;
291
+ BOT_STATES: DurableObjectNamespace<BotState>;
292
+ USER_CONFIGURATIONS: DurableObjectNamespace<UserConfiguration>;
293
+ DEPLOYMENT_POLICY: DurableObjectNamespace<DeploymentPolicy>;
294
+ /** One voice session object per User, `idFromName(userId)` (docs/voice.md). */
295
+ VOICE_ASSISTANTS: DurableObjectNamespace<VoiceAssistant>;
296
+ /** One object per Group Chat, `idFromName("<userId>:<groupId>")`. */
297
+ GROUP_CHATS: DurableObjectNamespace<GroupChat>;
298
+ /** The composer's dictation upstream. Absent closes dictation, visibly. */
299
+ OPENAI_API_KEY?: string;
300
+ /** The voice session itself: Gemini Live. Absent closes it, visibly. */
301
+ GEMINI_API_KEY?: string;
302
+ /** A local Live stand-in for the test harness; never set in production. */
303
+ VOICE_ASSISTANT_UPSTREAM_URL?: string;
304
+ VOICE_ASSISTANT_MODEL?: string;
305
+ /**
306
+ * The model that tidies a dictated transcript. Unset takes the ordinary
307
+ * default route; a deployment that wants a cheap, fast model for a job that
308
+ * is only ever "remove the ums" pins one here without a code change.
309
+ */
310
+ VOICE_DICTATION_CLEANUP_MODEL?: string;
311
+ /** A local dictation stand-in for the test harness; never set in production. */
312
+ VOICE_DICTATION_UPSTREAM_URL?: string;
313
+ COMPUTER_HOST: Fetcher;
314
+ /** Shared secret presented on every Computer host call. */
315
+ COMPUTER_HOST_TOKEN?: string;
316
+ /**
317
+ * The plugin build service. It is handed source and returns artifacts; this
318
+ * Worker keeps the R2 write and the hash verification, so the builder holds
319
+ * no authority of its own.
320
+ */
321
+ APPLET_BUILD: Fetcher;
322
+ /** Shared secret presented on every plugin build call. */
323
+ APPLET_BUILD_TOKEN?: string;
324
+ /**
325
+ * The identity store, on a build whose auth Package has one. The Access
326
+ * Package stores nothing and its deployment binds no database, which is why
327
+ * this is optional — and why nothing outside the Package reads it.
328
+ */
329
+ AUTH_DB?: D1Database;
330
+ DEFAULT_APPLICATION_HASH: string;
331
+ BETTER_AUTH_SECRET?: string;
332
+ BETTER_AUTH_URL?: string;
333
+ GOOGLE_CLIENT_ID?: string;
334
+ GOOGLE_CLIENT_SECRET?: string;
335
+ /** The Zero Trust team whose keys sign every Cloudflare Access token. */
336
+ ACCESS_TEAM_DOMAIN?: string;
337
+ /** The Access application's audience tag. */
338
+ ACCESS_AUD?: string;
339
+ /** The native door's signing key on the Access build; see `AUTH_PACKAGE_V1`. */
340
+ NATIVE_TOKEN_SECRET?: string;
341
+ /**
342
+ * Seals every Connection credential, and — through a key derived from it
343
+ * for that purpose alone — signs the `state` of an MCP server's sign-in.
344
+ */
345
+ CREDENTIAL_KEYRING?: string;
346
+ /** Signs every Routine webhook key. Absent closes the webhook door. */
347
+ ROUTINE_HOOK_SECRET?: string;
348
+ /**
349
+ * The domain Email Routing hands to this Worker's `email()` handler, from
350
+ * the deployment profile's `email`, and the one every Bot sends from.
351
+ * Absent, no Bot has an address, every message is refused and nothing is
352
+ * sent.
353
+ */
354
+ EMAIL_DOMAIN?: string;
355
+ /** The Connected apps provider key. Absent, no app can be connected. */
356
+ COMPOSIO_API_KEY?: string;
357
+ /**
358
+ * HMAC secret for Connected-app event deliveries. Absent closes the
359
+ * deployment-wide events door.
360
+ */
361
+ COMPOSIO_WEBHOOK_SECRET?: string;
362
+ /** The platform's web search key. Absent, a Bot has no `web_search`. */
363
+ BRAVE_SEARCH_API_KEY?: string;
364
+ /**
365
+ * Signs every machine token and pairing code. Absent closes the registered
366
+ * machine door: pairing, enrollment and every machine route answer 503
367
+ * rather than admitting a caller nothing could verify.
368
+ */
369
+ MACHINE_TOKEN_SECRET?: string;
370
+ ALLOW_DEVELOPMENT_AUTH?: string;
371
+ FROCKBOT_ADMIN_EMAILS?: string;
372
+ ALLOWED_CLIENT_ORIGINS?: string;
373
+ /** Authorizes `/api/debug/*`. Absent disables the surface entirely. */
374
+ DEBUG_TOKEN?: string;
375
+ /**
376
+ * The hosted Jev credential: Turn supervision, the routine-event rejector,
377
+ * and dictation tidy review. Required: without it no Turn runs.
378
+ */
379
+ JEV_API_KEY?: string;
380
+ /** Points Jev at a local stand-in; set by the test harness only. */
381
+ JEV_BASE_URL?: string;
382
+ }
383
+
384
+ /**
385
+ * The identity store of the sign-in Package this build deploys.
386
+ *
387
+ * Admission and the operator surface read the durable identity rather than the
388
+ * User id a caller presented, and which store that is belongs to the Package —
389
+ * the better-auth build's D1 `user` table, or, on the Access build, none at
390
+ * all. Constructed per lookup because both reads are a single query and neither
391
+ * needs sign-in itself to be configured.
392
+ */
393
+ function authIdentitiesV1(env: Env): AuthPackageIdentityStoreV1 {
394
+ return AUTH_PACKAGE_V1.create(env, { productName: BRAND_V1.productName });
395
+ }
396
+
397
+ /** The User Durable Object's account features, addressed by User. */
398
+ function userFeaturesStub(
399
+ env: Env,
400
+ userId: string,
401
+ ): {
402
+ readFeatures(input: unknown): Promise<unknown>;
403
+ setFeatures(input: unknown): Promise<unknown>;
404
+ } {
405
+ const id = env.USER_CONFIGURATIONS.idFromName(userId);
406
+ // SAFETY: Wrangler binds USER_CONFIGURATIONS to UserConfiguration; workers-types cannot infer its generated account features RPC surface.
407
+ return env.USER_CONFIGURATIONS.get(id) as unknown as {
408
+ readFeatures(input: unknown): Promise<unknown>;
409
+ setFeatures(input: unknown): Promise<unknown>;
410
+ };
411
+ }
412
+
413
+ /**
414
+ * The `/api/debug` surface, over the same durable records and identity store
415
+ * the gateway already holds. Without `DEBUG_TOKEN` it carries no token, and
416
+ * the routes 404.
417
+ */
418
+ function debugSurface(env: Env): DebugGatewaySurface {
419
+ return {
420
+ ...(env.DEBUG_TOKEN ? { token: env.DEBUG_TOKEN } : {}),
421
+ // A build whose auth Package stores no identity lists none: there is no
422
+ // roll of accounts to read, because Access re-establishes who a person is
423
+ // on every request. Every other route still answers for a User id the
424
+ // operator already has.
425
+ listUsers: async () =>
426
+ (await authIdentitiesV1(env).listStoredIdentities?.(50)) ?? [],
427
+ listBots: (userId) =>
428
+ userConfigurationStub(env, userId).listBots({ schemaVersion: 1, userId }),
429
+ snapshot: (userId, botId, query) =>
430
+ botStateStub(env, userId, botId).debugSnapshot(query),
431
+ voice: async (userId) =>
432
+ (await getAgentByName(env.VOICE_ASSISTANTS, userId)).debugSnapshot(),
433
+ isAdminUser: async (userId) => {
434
+ // The auth Package's store is the durable identity source. The path's
435
+ // User id is never trusted on its own: it must resolve to a stored email,
436
+ // and that identity is evaluated by the same allowlist policy as the
437
+ // signed-in gateway. A Package that stores nothing resolves nothing, so
438
+ // the one write on this surface is refused rather than granted on the
439
+ // strength of a User id somebody typed.
440
+ const identity = await authIdentitiesV1(env).storedIdentity?.(userId);
441
+ return (
442
+ identity != null &&
443
+ isDeploymentAdminV1(
444
+ { id: identity.id, email: identity.email, mode: "better-auth" },
445
+ env.FROCKBOT_ADMIN_EMAILS,
446
+ )
447
+ );
448
+ },
449
+ setAccountFeatures: async (userId, command) =>
450
+ decodeUserFeaturesV1(
451
+ rpcJsonSnapshotV1(
452
+ await userFeaturesStub(env, userId).setFeatures({
453
+ schemaVersion: 1,
454
+ userId,
455
+ command,
456
+ updatedBy: "operator",
457
+ }),
458
+ ),
459
+ ),
460
+ };
461
+ }
462
+
463
+ function allowedClientOrigins(env: Env): string[] | undefined {
464
+ const origins = (env.ALLOWED_CLIENT_ORIGINS ?? "")
465
+ .split(",")
466
+ .map((origin) => origin.trim())
467
+ .filter((origin) => origin.length > 0);
468
+ return origins.length > 0 ? origins : undefined;
469
+ }
470
+
471
+ interface UserScopedProps {
472
+ userId: string;
473
+ }
474
+
475
+ interface BotStateRpc extends BotConfigurationBinding {
476
+ deliverConnectEvent(request: {
477
+ schemaVersion: 1;
478
+ userId: string;
479
+ botId: string;
480
+ routineId: string;
481
+ eventId: string;
482
+ payload?: unknown;
483
+ }): Promise<
484
+ | { status: "accepted" | "duplicate"; fireId: string }
485
+ | { status: "dropped"; reason: string }
486
+ >;
487
+ readComputerPresence(): Promise<unknown>;
488
+ readComputerFrame(
489
+ contentHash: string,
490
+ ): Promise<{ bytesBase64: string } | null>;
491
+ executeComputerPresenceCommand(command: ComputerCommandV1): Promise<unknown>;
492
+ run(
493
+ command: BotTurnCommandRequestV1<OwnedBotTurnCommand>,
494
+ ): Promise<BotTurnResult>;
495
+ admitRun(
496
+ command: BotTurnCommandRequestV1<OwnedBotTurnCommand>,
497
+ ): Promise<{ schemaVersion: 1; runId: string }>;
498
+ listRuns(query: ClientRunListQueryV1): Promise<ClientRunListV1>;
499
+ debugSnapshot(query: BotDebugQueryV1): Promise<unknown>;
500
+ lookupRun(query: ClientRunLookupQueryV1): Promise<ClientRunLookupV1>;
501
+ runQuestions(query: ClientRunLookupQueryV1): Promise<ClientRunQuestionsV1>;
502
+ fenceRunAdmission(query: ClientRunLookupQueryV1): Promise<ClientRunLookupV1>;
503
+ listSkills(): Promise<ClientSkillCatalogV1>;
504
+ readWorkspaceFileV1(path: unknown): Promise<ClientWorkspaceFileV1>;
505
+ listNotifications(): Promise<BotNotificationIntent[]>;
506
+ acknowledgeNotification(notificationId: string): Promise<void>;
507
+ listApprovals(): Promise<ApprovalListViewV1>;
508
+ decideApproval(
509
+ approvalId: string,
510
+ command: ApprovalDecisionCommandV1,
511
+ ): Promise<ApprovalDecisionReceiptV1>;
512
+ listCards(): Promise<CardListViewV1>;
513
+ readCard(surfaceId: string): Promise<CardViewV1>;
514
+ cardAction(command: CardActionCommandV1): Promise<CardActionReceiptV1>;
515
+ submitSecret(
516
+ requestId: string,
517
+ command: SecretSubmitCommandV1,
518
+ ): Promise<unknown>;
519
+ readUnread(): Promise<BotUnreadViewV1>;
520
+ executeUnreadCommand(
521
+ command: BotUnreadCommandV1,
522
+ ): Promise<BotUnreadReceiptV1>;
523
+ stopRun(command: ClientRunStopCommandV1): Promise<ClientRunStopReceiptV1>;
524
+ }
525
+
526
+ /**
527
+ * The User Durable Object's RPC surface as this Worker uses it: the binding the
528
+ * gateway shares, plus this adapter's own seams.
529
+ */
530
+ interface UserConfigurationRpc extends UserConfigurationBinding {
531
+ readFlockBootstrap(request: {
532
+ schemaVersion: 1;
533
+ userId: string;
534
+ }): Promise<FlockBootstrapViewV1>;
535
+ listConnectTriggers(request: {
536
+ schemaVersion: 1;
537
+ userId: string;
538
+ }): Promise<ConnectTriggerOfferV1[]>;
539
+ handleConnectEvent(request: {
540
+ schemaVersion: 1;
541
+ userId: string;
542
+ event: ConnectEventV1;
543
+ }): Promise<{ status: "accepted" | "ignored"; fireId?: string }>;
544
+ }
545
+
546
+ type RpcBoundary<T> = {
547
+ [Key in keyof T]: T[Key] extends (...args: never[]) => infer Result
548
+ ? (input: unknown) => Result
549
+ : never;
550
+ };
551
+
552
+ /** The upload route's bindings: the object store, the converter, two objects. */
553
+ function uploadRouteDependenciesV1(env: Env): UploadRouteDependenciesV1 {
554
+ const ai = env.AI as unknown as Partial<DocumentConverterV1> | undefined;
555
+ return {
556
+ bucket: {
557
+ put: (key, value, options) => env.MEMORY_FILES.put(key, value, options),
558
+ get: (key) => env.MEMORY_FILES.get(key),
559
+ },
560
+ // A stand-in `AI` binding (local, CI) may convert nothing; plain text
561
+ // still reads without it.
562
+ ...(typeof ai?.toMarkdown === "function"
563
+ ? {
564
+ converter: {
565
+ toMarkdown: (files, options) => ai.toMarkdown!(files, options),
566
+ },
567
+ }
568
+ : {}),
569
+ botRegistered: async (userId, botId) =>
570
+ decodeBotMembershipViewV1(
571
+ await userConfigurationStub(env, userId).hasBot({
572
+ schemaVersion: 1,
573
+ userId,
574
+ botId,
575
+ }),
576
+ ).registered,
577
+ reserveQuota: (input) =>
578
+ // SAFETY: this binding names UserConfiguration; this is its reviewed RPC.
579
+ (
580
+ env.USER_CONFIGURATIONS.get(
581
+ env.USER_CONFIGURATIONS.idFromName(input.userId),
582
+ ) as unknown as Pick<UserConfiguration, "reserveUploadQuota">
583
+ ).reserveUploadQuota({ schemaVersion: 1, ...input }),
584
+ recordUpload: (input) =>
585
+ // SAFETY: this binding names BotState; this is its reviewed RPC. The
586
+ // name is decoded where the route read it, so no `#` reaches here.
587
+ (
588
+ env.BOT_STATES.get(
589
+ env.BOT_STATES.idFromName(
590
+ `${input.userId}:${decodeBotIdV1(input.botId, "bot id")}`,
591
+ ),
592
+ ) as unknown as Pick<BotState, "recordUploadV1">
593
+ ).recordUploadV1({ schemaVersion: 1, ...input }),
594
+ };
595
+ }
596
+
597
+ function botTurnRpcV1(command: BotTurnCommandRequestV1<OwnedBotTurnCommand>) {
598
+ return {
599
+ schemaVersion: 1 as const,
600
+ userId: command.userId,
601
+ botId: command.botId,
602
+ command: {
603
+ runId: command.runId,
604
+ sessionId: command.sessionId,
605
+ acceptedAt: command.acceptedAt,
606
+ text: command.text,
607
+ ...(command.retryOf ? { retryOf: command.retryOf } : {}),
608
+ ...(command.skills ? { skills: command.skills } : {}),
609
+ // Refs only across the door, whatever the caller held.
610
+ ...(command.attachments?.length
611
+ ? {
612
+ attachments: command.attachments.map(({ uploadId }) => ({
613
+ uploadId,
614
+ })),
615
+ }
616
+ : {}),
617
+ },
618
+ };
619
+ }
620
+
621
+ function botStateStub(env: Env, userId: string, botId: string): BotStateRpc {
622
+ // The one place a Bot Durable Object is named, and therefore the one place
623
+ // the name has to be beyond doubt. A Subagent Durable Object is the same
624
+ // class in this namespace under `<userId>:<botId>#task:<taskId>`, so a `#`
625
+ // reaching here from a path segment would let a caller name an object the
626
+ // directory never minted. `decodeBotIdV1` rejects it — this
627
+ // restates the check where the id becomes an object rather than trusting
628
+ // that every route above remembered to.
629
+ const id = env.BOT_STATES.idFromName(
630
+ `${userId}:${decodeBotIdV1(botId, "bot id")}`,
631
+ );
632
+ // SAFETY: Wrangler binds BOT_STATES to BotState; workers-types cannot infer its generated RPC surface.
633
+ const rpc = env.BOT_STATES.get(id) as unknown as RpcBoundary<BotStateRpc>;
634
+ return {
635
+ readComputerPresence: () =>
636
+ rpc.readComputerPresence({ schemaVersion: 1, userId, botId }),
637
+ readComputerFrame: (contentHash) =>
638
+ rpc.readComputerFrame({ schemaVersion: 1, userId, botId, contentHash }),
639
+ executeComputerPresenceCommand: (command) =>
640
+ rpc.executeComputerPresenceCommand({
641
+ schemaVersion: 1,
642
+ userId,
643
+ botId,
644
+ command,
645
+ }),
646
+ readAvatar: (request) => rpc.readAvatar(request),
647
+ updateAvatar: (request) => rpc.updateAvatar(request),
648
+ readVoice: (request) => rpc.readVoice(request),
649
+ updateVoice: (request) => rpc.updateVoice(request),
650
+ readLook: (request) => rpc.readLook(request),
651
+ updateLook: (request) => rpc.updateLook(request),
652
+ persistAssembledDocument: (request) =>
653
+ rpc.persistAssembledDocument(request),
654
+ readConfiguration: (request) => rpc.readConfiguration(request),
655
+ executeConfiguration: (request) => rpc.executeConfiguration(request),
656
+ readBotPluginsFrame: (request) => rpc.readBotPluginsFrame(request),
657
+ setBotPluginEnabled: (request) => rpc.setBotPluginEnabled(request),
658
+ executeBotPluginTool: (request) => rpc.executeBotPluginTool(request),
659
+ openFocusedPanel: (request) => rpc.openFocusedPanel(request),
660
+ setFocusedPanel: (request) => rpc.setFocusedPanel(request),
661
+ recordPanelDeviceUse: (request) => rpc.recordPanelDeviceUse(request),
662
+ recordPanelPageReport: (request) => rpc.recordPanelPageReport(request),
663
+ listRoutines: (request) => rpc.listRoutines(request),
664
+ readRoutinesFrame: (request) => rpc.readRoutinesFrame(request),
665
+ listTasks: (request) => rpc.listTasks(request),
666
+ readTask: (request) => rpc.readTask(request),
667
+ stopTask: (request) => rpc.stopTask(request),
668
+ executeRoutineCommand: (request) => rpc.executeRoutineCommand(request),
669
+ listRoutineRuns: (request) => rpc.listRoutineRuns(request),
670
+ deliverRoutineHook: (request) => rpc.deliverRoutineHook(request),
671
+ deliverConnectEvent: (request) => rpc.deliverConnectEvent(request),
672
+ deliverMachineResult: (request) => rpc.deliverMachineResult(request),
673
+ readRoutineRun: (request) => rpc.readRoutineRun(request),
674
+ listRoutineInbox: (request) => rpc.listRoutineInbox(request),
675
+ executeRoutineInboxCommand: (request) =>
676
+ rpc.executeRoutineInboxCommand(request),
677
+ listCompositionGenerations: (request) =>
678
+ rpc.listCompositionGenerations(request),
679
+ getCompositionGeneration: (request) =>
680
+ rpc.getCompositionGeneration(request),
681
+ revertComposition: (request) => rpc.revertComposition(request),
682
+ run: (command) => rpc.run(botTurnRpcV1(command)),
683
+ admitRun: (command) => rpc.admitRun(botTurnRpcV1(command)),
684
+ listRuns: (query) =>
685
+ rpc.listRuns({ schemaVersion: 1, userId, botId, query }),
686
+ debugSnapshot: (query) =>
687
+ rpc.debugSnapshot({ schemaVersion: 1, userId, botId, query }),
688
+ lookupRun: (query) =>
689
+ rpc.lookupRun({ schemaVersion: 1, userId, botId, query }),
690
+ runQuestions: (query) =>
691
+ rpc.runQuestions({ schemaVersion: 1, userId, botId, query }),
692
+ fenceRunAdmission: (query) =>
693
+ rpc.fenceRunAdmission({ schemaVersion: 1, userId, botId, query }),
694
+ listSkills: () => rpc.listSkills({ schemaVersion: 1, userId, botId }),
695
+ readWorkspaceFileV1: (path) =>
696
+ rpc.readWorkspaceFileV1({ schemaVersion: 1, userId, botId, path }),
697
+ listNotifications: () =>
698
+ rpc.listNotifications({ schemaVersion: 1, userId, botId }),
699
+ listApprovals: () => rpc.listApprovals({ schemaVersion: 1, userId, botId }),
700
+ decideApproval: (approvalId, command) =>
701
+ rpc.decideApproval({
702
+ schemaVersion: 1,
703
+ userId,
704
+ botId,
705
+ approvalId,
706
+ command,
707
+ }),
708
+ listCards: () => rpc.listCards({ schemaVersion: 1, userId, botId }),
709
+ readCard: (surfaceId) =>
710
+ rpc.readCard({ schemaVersion: 1, userId, botId, surfaceId }),
711
+ cardAction: (command) =>
712
+ rpc.cardAction({ schemaVersion: 1, userId, botId, command }),
713
+ submitSecret: (requestId, command) =>
714
+ rpc.submitSecret({ schemaVersion: 1, userId, botId, requestId, command }),
715
+ readUnread: () => rpc.readUnread({ schemaVersion: 1, userId, botId }),
716
+ executeUnreadCommand: (command) =>
717
+ rpc.executeUnreadCommand({ schemaVersion: 1, userId, botId, command }),
718
+ acknowledgeNotification: (notificationId) =>
719
+ rpc.acknowledgeNotification({
720
+ schemaVersion: 1,
721
+ userId,
722
+ botId,
723
+ notificationId,
724
+ }),
725
+ stopRun: (command) =>
726
+ rpc.stopRun({ schemaVersion: 1, userId, botId, command }),
727
+ };
728
+ }
729
+
730
+ function botStateObject(
731
+ env: Env,
732
+ userId: string,
733
+ botId: string,
734
+ ): DurableObjectStub {
735
+ const id = env.BOT_STATES.idFromName(
736
+ `${userId}:${decodeBotIdV1(botId, "bot id")}`,
737
+ );
738
+ return env.BOT_STATES.get(id);
739
+ }
740
+
741
+ function userConfigurationStub(env: Env, userId: string): UserConfigurationRpc {
742
+ const id = env.USER_CONFIGURATIONS.idFromName(userId);
743
+ // SAFETY: Wrangler binds USER_CONFIGURATIONS to UserConfiguration; workers-types cannot infer its RPC surface.
744
+ const rpc = env.USER_CONFIGURATIONS.get(
745
+ id,
746
+ ) as unknown as RpcBoundary<UserConfigurationRpc>;
747
+ return {
748
+ listBots: (request) => rpc.listBots(request),
749
+ listBotLifecycles: (request) => rpc.listBotLifecycles(request),
750
+ readFlockBootstrap: (request) => rpc.readFlockBootstrap(request),
751
+ executeBotLifecycle: (request) => rpc.executeBotLifecycle(request),
752
+ createBot: (request) => rpc.createBot(request),
753
+ updateBotAvatar: (request) => rpc.updateBotAvatar(request),
754
+ readBotVoice: (request) => rpc.readBotVoice(request),
755
+ updateBotVoice: (request) => rpc.updateBotVoice(request),
756
+ readBotLook: (request) => rpc.readBotLook(request),
757
+ updateBotLook: (request) => rpc.updateBotLook(request),
758
+ mirrorBotLook: (request) => rpc.mirrorBotLook(request),
759
+ getBotRegistration: (request) => rpc.getBotRegistration(request),
760
+ hasBot: (request) => rpc.hasBot(request),
761
+ readConnectionsFrame: (request) => rpc.readConnectionsFrame(request),
762
+ listSecrets: async (request) =>
763
+ decodeSecretListViewV1(await rpc.listSecrets(request)),
764
+ deleteSecret: (request) => rpc.deleteSecret(request),
765
+ readSettingsFrame: (request) => rpc.readSettingsFrame(request),
766
+ readSettingsOptions: (request) => rpc.readSettingsOptions(request),
767
+ changeSettings: (request) => rpc.changeSettings(request),
768
+ readConfiguration: (request) =>
769
+ rpc.readConfiguration({ ...request, view: request.view ?? 2 }),
770
+ executeConfiguration: (request) => rpc.executeConfiguration(request),
771
+ executeConnection: (request) => rpc.executeConnection(request),
772
+ listConnectTriggers: (request) => rpc.listConnectTriggers(request),
773
+ handleConnectEvent: (request) => rpc.handleConnectEvent(request),
774
+ lookupConnectionCommand: (request) => rpc.lookupConnectionCommand(request),
775
+ getConnection: (request) => rpc.getConnection(request),
776
+ leaseModelCredential: (request) => rpc.leaseModelCredential(request),
777
+ settleModelCredential: (request) => rpc.settleModelCredential(request),
778
+ };
779
+ }
780
+
781
+ interface DeploymentPolicyRpc {
782
+ claimEmailUsername(input: unknown): Promise<unknown>;
783
+ releaseEmailUsername(input: unknown): Promise<unknown>;
784
+ resolveEmailUsername(input: unknown): Promise<unknown>;
785
+ readEmailUsername(input: unknown): Promise<unknown>;
786
+ readPolicy(input: unknown): Promise<unknown>;
787
+ setAdmissionMode(input: unknown): Promise<unknown>;
788
+ readAccountAccess(input: unknown): Promise<unknown>;
789
+ setAccountAccess(input: unknown): Promise<unknown>;
790
+ inviteEmail(input: unknown): Promise<unknown>;
791
+ admitAccount(input: unknown): Promise<unknown>;
792
+ checkAccount(input: unknown): Promise<unknown>;
793
+ mayCreateIdentity(input: unknown): Promise<unknown>;
794
+ readModelRates(input: unknown): Promise<unknown>;
795
+ }
796
+
797
+ function deploymentPolicyStub(env: Env): DeploymentPolicyRpc {
798
+ return env.DEPLOYMENT_POLICY.getByName(
799
+ DEPLOYMENT_POLICY_SINGLETON_NAME,
800
+ ) as unknown as DeploymentPolicyRpc;
801
+ }
802
+
803
+ /**
804
+ * Every identity this build's sign-in Package produced is admitted.
805
+ *
806
+ * Cloudflare Access admits nobody the deployment's own policy did not, so on
807
+ * that build the policy *is* the allowlist: there is no authority to ask, no
808
+ * access record to read and no admission UI to show (ADR 0028). The hosted
809
+ * build's Package answers `authority` and nothing here applies to it.
810
+ */
811
+ const AUTH_PACKAGE_DECIDES_ADMISSION_V1 =
812
+ AUTH_PACKAGE_V1.admission === "package";
813
+ const ADMITTED_BY_AUTH_PACKAGE_V1: AccountAdmissionDecisionV1 = {
814
+ schemaVersion: 1,
815
+ admitted: true,
816
+ // The deployment is open to everyone its sign-in policy let through, which
817
+ // is what `open` says. No account is activated, because none is recorded.
818
+ basis: "open",
819
+ };
820
+
821
+ /**
822
+ * The one door into the beta-access authority for browser and native alike.
823
+ * An admin is answered here, without the authority, so a deployment whose
824
+ * authority is unreachable still lets its admins in to see why.
825
+ */
826
+ async function admitAccount(
827
+ env: Env,
828
+ identity: AdmissionIdentityV1,
829
+ ): Promise<AccountAdmissionDecisionV1> {
830
+ if (identity.isAdmin) {
831
+ return { schemaVersion: 1, admitted: true, basis: "admin" };
832
+ }
833
+ if (AUTH_PACKAGE_DECIDES_ADMISSION_V1) return ADMITTED_BY_AUTH_PACKAGE_V1;
834
+ return decodeAccountAdmissionDecisionV1(
835
+ rpcJsonSnapshotV1(await deploymentPolicyStub(env).admitAccount(identity)),
836
+ );
837
+ }
838
+
839
+ async function storedAdmissionIdentity(
840
+ env: Env,
841
+ userId: string,
842
+ ): Promise<AdmissionIdentityV1 | null> {
843
+ const identity = await authIdentitiesV1(env).storedIdentity?.(userId);
844
+ if (!identity) return null;
845
+ const email = accessEmailV1(identity.email);
846
+ return {
847
+ schemaVersion: 1,
848
+ userId,
849
+ ...(email === undefined ? {} : { email }),
850
+ emailVerified: identity.emailVerified,
851
+ isAdmin: isDeploymentAdminV1(
852
+ { id: identity.id, email: identity.email, mode: "better-auth" },
853
+ env.FROCKBOT_ADMIN_EMAILS,
854
+ ),
855
+ };
856
+ }
857
+
858
+ async function admitStoredAccount(
859
+ env: Env,
860
+ userId: string,
861
+ ): Promise<AccountAdmissionDecisionV1 | null> {
862
+ if (AUTH_PACKAGE_DECIDES_ADMISSION_V1) return ADMITTED_BY_AUTH_PACKAGE_V1;
863
+ const identity = await storedAdmissionIdentity(env, userId);
864
+ return identity ? admitAccount(env, identity) : null;
865
+ }
866
+
867
+ async function checkStoredAccount(
868
+ env: Env,
869
+ userId: string,
870
+ ): Promise<AccountAdmissionDecisionV1 | null> {
871
+ if (AUTH_PACKAGE_DECIDES_ADMISSION_V1) return ADMITTED_BY_AUTH_PACKAGE_V1;
872
+ const identity = await storedAdmissionIdentity(env, userId);
873
+ if (!identity) return null;
874
+ if (identity.isAdmin) {
875
+ return { schemaVersion: 1, admitted: true, basis: "admin" };
876
+ }
877
+ return decodeAccountAdmissionDecisionV1(
878
+ rpcJsonSnapshotV1(await deploymentPolicyStub(env).checkAccount(identity)),
879
+ );
880
+ }
881
+
882
+ async function externalAccountRefusal(
883
+ env: Env,
884
+ userId: string,
885
+ ): Promise<{ status: number; message: string } | undefined> {
886
+ if (developmentAuthAllowed(env)) return;
887
+ let decision;
888
+ try {
889
+ decision = await checkStoredAccount(env, userId);
890
+ } catch {
891
+ return { status: 503, message: ACCOUNT_ADMISSION_UNAVAILABLE_MESSAGE };
892
+ }
893
+ if (!decision)
894
+ return { status: 401, message: "Account identity is unavailable" };
895
+ if (!decision.admitted) {
896
+ return {
897
+ status: 403,
898
+ message: admissionRefusalCopyV1(BRAND_V1.productName)[decision.reason]
899
+ .title,
900
+ };
901
+ }
902
+ }
903
+
904
+ /**
905
+ * Whether better-auth may write a new identity. The same authority as
906
+ * admission, asked earlier; admins bypass it, and all other candidates must
907
+ * satisfy the identity-creation rule in `account-admission.ts`.
908
+ */
909
+ async function mayCreateIdentity(
910
+ env: Env,
911
+ candidate: AuthIdentityCandidateV1,
912
+ ): Promise<boolean> {
913
+ const email = accessEmailV1(candidate.email);
914
+ if (email === undefined) return false;
915
+ if (
916
+ isDeploymentAdminV1(
917
+ { id: email, email, mode: "better-auth" },
918
+ env.FROCKBOT_ADMIN_EMAILS,
919
+ )
920
+ ) {
921
+ return true;
922
+ }
923
+ return (
924
+ (await deploymentPolicyStub(env).mayCreateIdentity({
925
+ schemaVersion: 1,
926
+ email,
927
+ emailVerified: candidate.emailVerified,
928
+ isAdmin: false,
929
+ })) === true
930
+ );
931
+ }
932
+
933
+ function developmentAuthAllowed(env: Env): boolean {
934
+ return env.ALLOW_DEVELOPMENT_AUTH === "true";
935
+ }
936
+
937
+ /**
938
+ * Where the app may be sent back after sign-in: this deployment's App Links,
939
+ * plus the development scheme on a stack that allows development auth — the flag
940
+ * production's secret gate refuses.
941
+ */
942
+ function nativeReturnUrisFor(env: Env, origin: string): readonly string[] {
943
+ return [
944
+ ...nativeReturnUris(env.NATIVE_SLICE_2_AUTH, origin),
945
+ ...(developmentAuthAllowed(env)
946
+ ? [nativeDevelopmentReturnUriV1(BRAND_V1)]
947
+ : []),
948
+ ];
949
+ }
950
+
951
+ /**
952
+ * The User Durable Object's transcript-index RPCs.
953
+ *
954
+ * Narrow and separate from `UserConfigurationBinding`: search is one Package's
955
+ * Contribution, and the generic configuration binding every gateway adapter
956
+ * implements has no business growing a method for it.
957
+ */
958
+ interface UserSearchRpc {
959
+ searchTranscripts(input: unknown): Promise<unknown>;
960
+ rebuildSearchIndex(input: unknown): Promise<unknown>;
961
+ }
962
+
963
+ /**
964
+ * The User Durable Object's registered-machine RPCs.
965
+ *
966
+ * Narrow and separate from `UserConfigurationBinding` for the same reason the
967
+ * transcript index's are: the registry is one Package's Contribution, and the
968
+ * generic configuration binding every gateway adapter implements has no
969
+ * business growing a method for somebody's laptop.
970
+ */
971
+ interface UserMachineRpc {
972
+ createMachinePairing(input: unknown): Promise<unknown>;
973
+ enrollMachine(input: unknown): Promise<unknown>;
974
+ claimMachineCommand(input: unknown): Promise<unknown>;
975
+ recordMachineResult(input: unknown): Promise<unknown>;
976
+ readMachineModule(input: unknown): Promise<{ found: boolean }>;
977
+ recordMachineModuleReports(input: unknown): Promise<unknown>;
978
+ claimMachineModuleCall(input: unknown): Promise<unknown>;
979
+ recordMachineModuleCallResult(input: unknown): Promise<unknown>;
980
+ recordMachineModuleEvents(input: unknown): Promise<unknown>;
981
+ takeMachineDeliveries(input: unknown): Promise<unknown>;
982
+ listMachines(input: unknown): Promise<unknown>;
983
+ revokeMachine(input: unknown): Promise<unknown>;
984
+ }
985
+
986
+ /**
987
+ * Drain a User's finished machine commands into the Bots that asked for them.
988
+ *
989
+ * Best effort by construction. The result is already durable and
990
+ * `machine_command_check` reads it in full, so a Bot that has since been
991
+ * deleted — or one that cannot be reached this second — costs a preamble line
992
+ * and no fact, and must never fail the machine's own POST.
993
+ */
994
+ async function deliverMachineResults(env: Env, userId: string): Promise<void> {
995
+ let deliveries: MachineResultDeliveryV1[];
996
+ try {
997
+ deliveries = (
998
+ (await userMachineStub(env, userId).takeMachineDeliveries({
999
+ schemaVersion: 1,
1000
+ userId,
1001
+ })) as unknown[]
1002
+ ).map((value) => decodeMachineResultDeliveryV1(value));
1003
+ } catch {
1004
+ return;
1005
+ }
1006
+ for (const delivery of deliveries) {
1007
+ try {
1008
+ await botStateStub(env, userId, delivery.botId).deliverMachineResult({
1009
+ schemaVersion: 1,
1010
+ userId,
1011
+ botId: delivery.botId,
1012
+ delivery,
1013
+ });
1014
+ } catch {
1015
+ // See above: the durable answer is already recorded.
1016
+ }
1017
+ }
1018
+ }
1019
+
1020
+ function userMachineStub(env: Env, userId: string): UserMachineRpc {
1021
+ const id = env.USER_CONFIGURATIONS.idFromName(userId);
1022
+ // SAFETY: Wrangler binds USER_CONFIGURATIONS to UserConfiguration; workers-types cannot infer its RPC surface.
1023
+ const rpc = env.USER_CONFIGURATIONS.get(id) as unknown as UserMachineRpc;
1024
+ return rpc;
1025
+ }
1026
+
1027
+ /**
1028
+ * The User Durable Object's audit RPCs. Narrow and separate for the same
1029
+ * reason the transcript index's are.
1030
+ */
1031
+ interface UserAuditRpc {
1032
+ readAuditEntries(input: unknown): Promise<unknown>;
1033
+ readAuditActivity(input: unknown): Promise<unknown>;
1034
+ rebuildAuditIndex(input: unknown): Promise<unknown>;
1035
+ }
1036
+
1037
+ /** The User Durable Object's inbound email RPCs. */
1038
+ interface UserInboundEmailRpc {
1039
+ readInboundEmail(input: unknown): Promise<unknown>;
1040
+ setBotEmailEnabled(input: unknown): Promise<unknown>;
1041
+ commandInboundEmailSender(input: unknown): Promise<unknown>;
1042
+ routeInboundEmail(input: unknown): Promise<unknown>;
1043
+ }
1044
+
1045
+ function userInboundEmailStub(env: Env, userId: string): UserInboundEmailRpc {
1046
+ // SAFETY: Wrangler binds USER_CONFIGURATIONS to UserConfiguration; workers-types cannot infer its RPC surface.
1047
+ return env.USER_CONFIGURATIONS.get(
1048
+ env.USER_CONFIGURATIONS.idFromName(userId),
1049
+ ) as unknown as UserInboundEmailRpc;
1050
+ }
1051
+
1052
+ /**
1053
+ * The address the User signs in with, when the identity provider verified
1054
+ * it: the one sender that needs no code, because signing in already proved it.
1055
+ */
1056
+ async function emailSignInV1(
1057
+ env: Env,
1058
+ userId: string,
1059
+ ): Promise<string | undefined> {
1060
+ const identity = await authIdentitiesV1(env).storedIdentity?.(userId);
1061
+ return identity?.emailVerified
1062
+ ? normalizeSenderAddressV1(identity.email)
1063
+ : undefined;
1064
+ }
1065
+
1066
+ /** A command's answer from the User object, as the settings route reads it. */
1067
+ function inboundEmailCommandOutcomeV1(
1068
+ answer: unknown,
1069
+ ): { status: "applied" } | { status: "rejected"; reason: string } {
1070
+ const outcome = rpcJsonSnapshotV1(answer) as {
1071
+ status?: unknown;
1072
+ reason?: unknown;
1073
+ };
1074
+ if (outcome.status === "applied") return { status: "applied" };
1075
+ return {
1076
+ status: "rejected",
1077
+ reason:
1078
+ typeof outcome.reason === "string"
1079
+ ? outcome.reason.slice(0, 300)
1080
+ : "That couldn’t be changed.",
1081
+ };
1082
+ }
1083
+
1084
+ /**
1085
+ * What the `email()` handler reaches: the directory, the User's object, the
1086
+ * uploads store and the Bot's email door. The handler itself decides the
1087
+ * order (`app/email/inbound.ts`).
1088
+ */
1089
+ function inboundEmailHostV1(env: Env): InboundEmailHostV1 {
1090
+ const domain = emailDomainV1(env);
1091
+ return {
1092
+ ...(domain ? { domain } : {}),
1093
+ resolveUsername: async (username) => {
1094
+ const { userId } = rpcJsonSnapshotV1(
1095
+ await deploymentPolicyStub(env).resolveEmailUsername({
1096
+ schemaVersion: 1,
1097
+ username,
1098
+ }),
1099
+ ) as { userId?: unknown };
1100
+ return typeof userId === "string" ? userId : undefined;
1101
+ },
1102
+ signInEmail: (userId) => emailSignInV1(env, userId),
1103
+ accountRefusal: async (userId) =>
1104
+ (await externalAccountRefusal(env, userId))?.message,
1105
+ route: async (userId, request) =>
1106
+ decodeInboundEmailRouteDecisionV1(
1107
+ rpcJsonSnapshotV1(
1108
+ await userInboundEmailStub(env, userId).routeInboundEmail({
1109
+ schemaVersion: 1,
1110
+ userId,
1111
+ ...request,
1112
+ }),
1113
+ ),
1114
+ ),
1115
+ storeAttachment: async (userId, botId, file) => {
1116
+ const stored = await storeUploadV1(uploadRouteDependenciesV1(env), {
1117
+ userId,
1118
+ botId,
1119
+ name: file.name,
1120
+ declaredType: file.mediaType,
1121
+ bytes: file.bytes,
1122
+ });
1123
+ return stored.status === "stored"
1124
+ ? { status: "stored", uploadId: stored.upload.uploadId }
1125
+ : { status: "refused", reason: stored.reason };
1126
+ },
1127
+ admit: async (userId, botId, command) => {
1128
+ // SAFETY: Wrangler binds BOT_STATES to BotState; this is its reviewed email door.
1129
+ const bot = env.BOT_STATES.get(
1130
+ env.BOT_STATES.idFromName(
1131
+ `${userId}:${decodeBotIdV1(botId, "bot id")}`,
1132
+ ),
1133
+ ) as unknown as { admitEmailTurn(input: unknown): Promise<unknown> };
1134
+ await bot.admitEmailTurn({
1135
+ schemaVersion: 1,
1136
+ userId,
1137
+ botId,
1138
+ command: {
1139
+ runId: command.runId,
1140
+ sessionId: `${userId}:${botId}`,
1141
+ acceptedAt: new Date().toISOString(),
1142
+ text: command.text,
1143
+ ...(command.attachments.length > 0
1144
+ ? { attachments: command.attachments }
1145
+ : {}),
1146
+ origin: { kind: "email", messageId: command.messageId },
1147
+ },
1148
+ });
1149
+ },
1150
+ };
1151
+ }
1152
+
1153
+ function userAuditStub(env: Env, userId: string): UserAuditRpc {
1154
+ const id = env.USER_CONFIGURATIONS.idFromName(userId);
1155
+ // SAFETY: Wrangler binds USER_CONFIGURATIONS to UserConfiguration; workers-types cannot infer its generated Audit RPC surface.
1156
+ return env.USER_CONFIGURATIONS.get(id) as unknown as UserAuditRpc;
1157
+ }
1158
+
1159
+ function userSearchStub(env: Env, userId: string): UserSearchRpc {
1160
+ const id = env.USER_CONFIGURATIONS.idFromName(userId);
1161
+ // SAFETY: Wrangler binds USER_CONFIGURATIONS to UserConfiguration; workers-types cannot infer its generated Search RPC surface.
1162
+ return env.USER_CONFIGURATIONS.get(id) as unknown as UserSearchRpc;
1163
+ }
1164
+
1165
+ function decodeUserBotTurnRpcV1(input: unknown) {
1166
+ return decodeRpcEnvelopeV1(input, {
1167
+ botId: rpcBotId,
1168
+ command: rpcObject(
1169
+ {
1170
+ runId: rpcIdentifier,
1171
+ sessionId: rpcString(257),
1172
+ acceptedAt: rpcString(64),
1173
+ text: rpcTurnText(100_000),
1174
+ },
1175
+ // The same optional members the Bot Durable Object's door accepts, so
1176
+ // nothing the composer sends is refused one door earlier.
1177
+ rpcBotTurnCommandOptionalsV1,
1178
+ ),
1179
+ });
1180
+ }
1181
+
1182
+ function decodeUserBotRunLookupRpcV1(input: unknown): {
1183
+ botId: string;
1184
+ query: ClientRunLookupQueryV1;
1185
+ } {
1186
+ const request = decodeRpcEnvelopeV1(input, {
1187
+ botId: rpcBotId,
1188
+ query: rpcDecoded(decodeClientRunLookupQueryV1),
1189
+ });
1190
+ return {
1191
+ botId: request.botId as string,
1192
+ query: request.query as ClientRunLookupQueryV1,
1193
+ };
1194
+ }
1195
+
1196
+ export class UserBotState extends WorkerEntrypoint<Env, UserScopedProps> {
1197
+ async assertRegistered(input: unknown): Promise<void> {
1198
+ const request = decodeRpcEnvelopeV1(input, { botId: rpcBotId });
1199
+ const botId = request.botId as string;
1200
+ const membership = decodeBotMembershipViewV1(
1201
+ await userConfigurationStub(this.env, this.ctx.props.userId).hasBot({
1202
+ schemaVersion: 1,
1203
+ userId: this.ctx.props.userId,
1204
+ botId,
1205
+ }),
1206
+ );
1207
+ if (!membership.registered) {
1208
+ const error = new Error(`Bot "${botId}" is not registered`);
1209
+ error.name = "BotNotFoundError";
1210
+ throw error;
1211
+ }
1212
+ const lifecycles = decodeBotLifecycleDirectoryViewV1(
1213
+ await userConfigurationStub(
1214
+ this.env,
1215
+ this.ctx.props.userId,
1216
+ ).listBotLifecycles({
1217
+ schemaVersion: 1,
1218
+ userId: this.ctx.props.userId,
1219
+ }),
1220
+ );
1221
+ if (
1222
+ lifecycles.lifecycles.find((item) => item.botId === botId)?.status ===
1223
+ "archived"
1224
+ ) {
1225
+ const error = new Error(`Bot "${botId}" is archived`);
1226
+ error.name = "BotArchivedError";
1227
+ throw error;
1228
+ }
1229
+ }
1230
+
1231
+ async run(input: unknown): Promise<BotTurnResult> {
1232
+ const request = decodeUserBotTurnRpcV1(input);
1233
+ const command = request.command as BotTurnCommandRequestV1<BotTurnCommand>;
1234
+ requireTurnCommandContentV1(command);
1235
+ return botStateStub(
1236
+ this.env,
1237
+ this.ctx.props.userId,
1238
+ request.botId as string,
1239
+ ).run({
1240
+ ...command,
1241
+ userId: this.ctx.props.userId,
1242
+ botId: request.botId as string,
1243
+ });
1244
+ }
1245
+
1246
+ async admitRun(input: unknown): Promise<{ schemaVersion: 1; runId: string }> {
1247
+ const request = decodeUserBotTurnRpcV1(input);
1248
+ const command = request.command as BotTurnCommandRequestV1<BotTurnCommand>;
1249
+ requireTurnCommandContentV1(command);
1250
+ return botStateStub(
1251
+ this.env,
1252
+ this.ctx.props.userId,
1253
+ request.botId as string,
1254
+ ).admitRun({
1255
+ ...command,
1256
+ userId: this.ctx.props.userId,
1257
+ botId: request.botId as string,
1258
+ });
1259
+ }
1260
+
1261
+ async listRuns(input: unknown): Promise<ClientRunListV1> {
1262
+ const request = decodeRpcEnvelopeV1(input, {
1263
+ botId: rpcBotId,
1264
+ query: rpcDecoded(decodeClientRunListQueryV1),
1265
+ });
1266
+ return botStateStub(
1267
+ this.env,
1268
+ this.ctx.props.userId,
1269
+ request.botId as string,
1270
+ ).listRuns(request.query as ClientRunListQueryV1);
1271
+ }
1272
+
1273
+ async lookupRun(input: unknown): Promise<ClientRunLookupV1> {
1274
+ const request = decodeUserBotRunLookupRpcV1(input);
1275
+ return botStateStub(
1276
+ this.env,
1277
+ this.ctx.props.userId,
1278
+ request.botId,
1279
+ ).lookupRun(request.query);
1280
+ }
1281
+
1282
+ async runQuestions(input: unknown): Promise<ClientRunQuestionsV1> {
1283
+ const request = decodeUserBotRunLookupRpcV1(input);
1284
+ return botStateStub(
1285
+ this.env,
1286
+ this.ctx.props.userId,
1287
+ request.botId,
1288
+ ).runQuestions(request.query);
1289
+ }
1290
+
1291
+ async fenceRunAdmission(input: unknown): Promise<ClientRunLookupV1> {
1292
+ const request = decodeUserBotRunLookupRpcV1(input);
1293
+ return botStateStub(
1294
+ this.env,
1295
+ this.ctx.props.userId,
1296
+ request.botId,
1297
+ ).fenceRunAdmission(request.query);
1298
+ }
1299
+
1300
+ async listSkills(input: unknown): Promise<ClientSkillCatalogV1> {
1301
+ const request = decodeRpcEnvelopeV1(input, { botId: rpcBotId });
1302
+ return botStateStub(
1303
+ this.env,
1304
+ this.ctx.props.userId,
1305
+ request.botId as string,
1306
+ ).listSkills();
1307
+ }
1308
+
1309
+ async readWorkspaceFileV1(input: unknown): Promise<ClientWorkspaceFileV1> {
1310
+ const request = decodeRpcEnvelopeV1(input, {
1311
+ botId: rpcBotId,
1312
+ path: rpcDecodedValue,
1313
+ });
1314
+ return botStateStub(
1315
+ this.env,
1316
+ this.ctx.props.userId,
1317
+ request.botId as string,
1318
+ ).readWorkspaceFileV1(request.path);
1319
+ }
1320
+
1321
+ async listNotifications(input: unknown): Promise<BotNotificationIntent[]> {
1322
+ const request = decodeRpcEnvelopeV1(input, { botId: rpcBotId });
1323
+ return botStateStub(
1324
+ this.env,
1325
+ this.ctx.props.userId,
1326
+ request.botId as string,
1327
+ ).listNotifications();
1328
+ }
1329
+
1330
+ /**
1331
+ * The Bot's approvals. Decoded at the seam, like every other read that
1332
+ * crosses from a Durable Object into the gateway.
1333
+ */
1334
+ async listApprovals(input: unknown): Promise<ApprovalListViewV1> {
1335
+ const request = decodeRpcEnvelopeV1(input, { botId: rpcBotId });
1336
+ return decodeApprovalListViewV1(
1337
+ await botStateStub(
1338
+ this.env,
1339
+ this.ctx.props.userId,
1340
+ request.botId as string,
1341
+ ).listApprovals(),
1342
+ );
1343
+ }
1344
+
1345
+ /** One decision, recorded durably before this answers. */
1346
+ async decideApproval(input: unknown): Promise<ApprovalDecisionReceiptV1> {
1347
+ const request = decodeRpcEnvelopeV1(input, {
1348
+ botId: rpcBotId,
1349
+ approvalId: rpcIdentifier,
1350
+ command: rpcDecoded(decodeApprovalDecisionCommandV1),
1351
+ });
1352
+ return decodeApprovalDecisionReceiptV1(
1353
+ await botStateStub(
1354
+ this.env,
1355
+ this.ctx.props.userId,
1356
+ request.botId as string,
1357
+ ).decideApproval(
1358
+ request.approvalId as string,
1359
+ request.command as ApprovalDecisionCommandV1,
1360
+ ),
1361
+ );
1362
+ }
1363
+
1364
+ /** The Bot's Cards, decoded at the seam like every other crossing read. */
1365
+ async listCards(input: unknown): Promise<CardListViewV1> {
1366
+ const request = decodeRpcEnvelopeV1(input, { botId: rpcBotId });
1367
+ return decodeCardListViewV1(
1368
+ await botStateStub(
1369
+ this.env,
1370
+ this.ctx.props.userId,
1371
+ request.botId as string,
1372
+ ).listCards(),
1373
+ );
1374
+ }
1375
+
1376
+ /** One Card by its id, for a surface the listing's byte budget left out. */
1377
+ async readCard(input: unknown): Promise<CardViewV1> {
1378
+ const request = decodeRpcEnvelopeV1(input, {
1379
+ botId: rpcBotId,
1380
+ surfaceId: rpcDecoded(decodeCardSurfaceIdV1),
1381
+ });
1382
+ return decodeCardViewV1(
1383
+ await botStateStub(
1384
+ this.env,
1385
+ this.ctx.props.userId,
1386
+ request.botId as string,
1387
+ ).readCard(request.surfaceId as string),
1388
+ );
1389
+ }
1390
+
1391
+ /** One action on one Card, routed by the kernel before this answers. */
1392
+ async cardAction(input: unknown): Promise<CardActionReceiptV1> {
1393
+ const request = decodeRpcEnvelopeV1(input, {
1394
+ botId: rpcBotId,
1395
+ command: rpcDecoded(decodeCardActionCommandV1),
1396
+ });
1397
+ return decodeCardActionReceiptV1(
1398
+ await botStateStub(
1399
+ this.env,
1400
+ this.ctx.props.userId,
1401
+ request.botId as string,
1402
+ ).cardAction(request.command as CardActionCommandV1),
1403
+ );
1404
+ }
1405
+
1406
+ /**
1407
+ * A value typed on a secret-request card. Passed through, never read: the
1408
+ * Bot checks the request and the User object seals the value.
1409
+ */
1410
+ async submitSecret(input: unknown): Promise<SecretSubmitReceiptV1> {
1411
+ const request = decodeRpcEnvelopeV1(input, {
1412
+ botId: rpcBotId,
1413
+ requestId: rpcPattern(/^secret-request-[0-9a-f]{32}$/, 128),
1414
+ command: rpcDecoded(decodeSecretSubmitCommandV1),
1415
+ });
1416
+ return decodeSecretSubmitReceiptV1(
1417
+ await botStateStub(
1418
+ this.env,
1419
+ this.ctx.props.userId,
1420
+ request.botId as string,
1421
+ ).submitSecret(
1422
+ request.requestId as string,
1423
+ request.command as SecretSubmitCommandV1,
1424
+ ),
1425
+ );
1426
+ }
1427
+
1428
+ async acknowledgeNotification(input: unknown): Promise<void> {
1429
+ const request = decodeRpcEnvelopeV1(input, {
1430
+ botId: rpcBotId,
1431
+ notificationId: rpcIdentifier,
1432
+ });
1433
+ return botStateStub(
1434
+ this.env,
1435
+ this.ctx.props.userId,
1436
+ request.botId as string,
1437
+ ).acknowledgeNotification(request.notificationId as string);
1438
+ }
1439
+
1440
+ async stopRun(input: unknown): Promise<ClientRunStopReceiptV1> {
1441
+ const request = decodeRpcEnvelopeV1(input, {
1442
+ botId: rpcBotId,
1443
+ command: rpcDecoded(decodeClientRunStopCommandV1),
1444
+ });
1445
+ return botStateStub(
1446
+ this.env,
1447
+ this.ctx.props.userId,
1448
+ request.botId as string,
1449
+ ).stopRun(request.command as ClientRunStopCommandV1);
1450
+ }
1451
+ }
1452
+
1453
+ class R2ApplicationArtifacts implements ApplicationArtifactStore {
1454
+ constructor(private readonly bucket: R2Bucket) {}
1455
+
1456
+ async load(applicationHash: string): Promise<string> {
1457
+ const object = await this.bucket.get(`applications/${applicationHash}.mjs`);
1458
+ if (!object) {
1459
+ throw new Error(
1460
+ `application artifact "${applicationHash}" was not found`,
1461
+ );
1462
+ }
1463
+ return object.text();
1464
+ }
1465
+
1466
+ async loadPluginPage(contentHash: string): Promise<string | undefined> {
1467
+ const object = await this.bucket.get(pluginPageKeyV1(contentHash));
1468
+ return object ? object.text() : undefined;
1469
+ }
1470
+ }
1471
+
1472
+ /**
1473
+ * Projects one Bot's durable settings onto the Flock identity DTO. The Bot
1474
+ * Durable Object stays the authority: this is a read-through view, so the
1475
+ * immutable registration seed never has to carry mutable identity.
1476
+ */
1477
+ function botIdentityView(
1478
+ botId: string,
1479
+ settings: BotSettingsViewV1,
1480
+ ): BotIdentityViewV1 {
1481
+ const profile = settings.profile;
1482
+ return {
1483
+ schemaVersion: 1,
1484
+ botId,
1485
+ name: profile.name,
1486
+ namedBy: profile.namedBy ?? "user",
1487
+ hiddenFromSidebar: profile.hiddenFromSidebar === true,
1488
+ ...(profile.pinnedAt === undefined ? {} : { pinnedAt: profile.pinnedAt }),
1489
+ ...(profile.sidebarOrder === undefined
1490
+ ? {}
1491
+ : { sidebarOrder: profile.sidebarOrder }),
1492
+ };
1493
+ }
1494
+
1495
+ async function listBotIdentities(
1496
+ env: Env,
1497
+ userId: string,
1498
+ ): Promise<BotIdentityDirectoryViewV1> {
1499
+ const directory = decodeDirectoryViewV1(
1500
+ rpcJsonSnapshotV1(
1501
+ await userConfigurationStub(env, userId).listBots({
1502
+ schemaVersion: 1,
1503
+ userId,
1504
+ }),
1505
+ ),
1506
+ );
1507
+ // The directory is already bounded to FLOCK_DIRECTORY_LIMIT; the slice makes
1508
+ // the fan-out bound explicit at the point that pays for it.
1509
+ const identities = await Promise.all(
1510
+ directory.bots.slice(0, FLOCK_DIRECTORY_LIMIT).map(async (bot) =>
1511
+ botIdentityView(
1512
+ bot.botId,
1513
+ (await botStateStub(env, userId, bot.botId).readConfiguration({
1514
+ schemaVersion: 1,
1515
+ userId,
1516
+ botId: bot.botId,
1517
+ })) as BotSettingsViewV1,
1518
+ ),
1519
+ ),
1520
+ );
1521
+ return decodeBotIdentityDirectoryViewV1({ schemaVersion: 1, identities });
1522
+ }
1523
+
1524
+ /**
1525
+ * The Bots a fan-out reads: registered, not archived, and bounded by the same
1526
+ * `FLOCK_DIRECTORY_LIMIT` the identity directory pays for.
1527
+ */
1528
+ async function fanOutBotIds(env: Env, userId: string): Promise<string[]> {
1529
+ const [directory, lifecycles] = await Promise.all([
1530
+ userConfigurationStub(env, userId)
1531
+ .listBots({ schemaVersion: 1, userId })
1532
+ .then((value) => decodeDirectoryViewV1(rpcJsonSnapshotV1(value))),
1533
+ userConfigurationStub(env, userId)
1534
+ .listBotLifecycles({ schemaVersion: 1, userId })
1535
+ .then((value) =>
1536
+ decodeBotLifecycleDirectoryViewV1(rpcJsonSnapshotV1(value)),
1537
+ ),
1538
+ ]);
1539
+ const archived = new Set(
1540
+ lifecycles.lifecycles
1541
+ .filter((entry) => entry.status === "archived")
1542
+ .map((entry) => entry.botId),
1543
+ );
1544
+ return directory.bots
1545
+ .slice(0, FLOCK_DIRECTORY_LIMIT)
1546
+ .map((bot) => bot.botId)
1547
+ .filter((botId) => !archived.has(botId));
1548
+ }
1549
+
1550
+ /** Unread for the whole sidebar in one round trip. */
1551
+ async function listBotUnread(
1552
+ env: Env,
1553
+ userId: string,
1554
+ ): Promise<BotUnreadDirectoryViewV1> {
1555
+ const botIds = await fanOutBotIds(env, userId);
1556
+ const unread = await Promise.all(
1557
+ botIds.map((botId) =>
1558
+ botStateStub(env, userId, botId)
1559
+ .readUnread()
1560
+ .then((value) => rpcJsonSnapshotV1(value)),
1561
+ ),
1562
+ );
1563
+ return decodeBotUnreadDirectoryViewV1({ schemaVersion: 1, unread });
1564
+ }
1565
+
1566
+ /**
1567
+ * Pending intents across every non-archived Bot. Acknowledgement stays
1568
+ * per-Bot: this route only makes a background Bot's completion visible.
1569
+ */
1570
+ async function listBotNotifications(
1571
+ env: Env,
1572
+ userId: string,
1573
+ ): Promise<BotNotificationDirectoryViewV1> {
1574
+ const botIds = await fanOutBotIds(env, userId);
1575
+ const perBot = await Promise.all(
1576
+ botIds.map(async (botId) =>
1577
+ (await botStateStub(env, userId, botId).listNotifications()).map(
1578
+ (intent) => ({
1579
+ schemaVersion: 1 as const,
1580
+ botId,
1581
+ ...rpcJsonSnapshotV1(intent),
1582
+ }),
1583
+ ),
1584
+ ),
1585
+ );
1586
+ return decodeBotNotificationDirectoryViewV1({
1587
+ schemaVersion: 1,
1588
+ notifications: perBot.flat(),
1589
+ });
1590
+ }
1591
+
1592
+ async function executeBotUnreadCommand(
1593
+ env: Env,
1594
+ userId: string,
1595
+ botId: string,
1596
+ command: BotUnreadCommandV1,
1597
+ ): Promise<BotUnreadReceiptV1> {
1598
+ // Membership first: a Bot this User does not own is not found, never marked.
1599
+ const membership = decodeBotMembershipViewV1(
1600
+ rpcJsonSnapshotV1(
1601
+ await userConfigurationStub(env, userId).hasBot({
1602
+ schemaVersion: 1,
1603
+ userId,
1604
+ botId,
1605
+ }),
1606
+ ),
1607
+ );
1608
+ if (!membership.registered) throw new BotNotFoundError(botId);
1609
+ return decodeBotUnreadReceiptV1(
1610
+ rpcJsonSnapshotV1(
1611
+ await botStateStub(env, userId, botId).executeUnreadCommand(command),
1612
+ ),
1613
+ );
1614
+ }
1615
+
1616
+ /**
1617
+ * Proves User-to-Bot membership before the Bot Durable Object is named.
1618
+ * Unknown and foreign Bots therefore share one 404 and cannot cause even an
1619
+ * empty Bot object — or a Computer intent within one — to be created.
1620
+ */
1621
+ async function ownedComputerBotState(
1622
+ env: Env,
1623
+ userId: string,
1624
+ botId: string,
1625
+ ): Promise<BotStateRpc> {
1626
+ const membership = decodeBotMembershipViewV1(
1627
+ rpcJsonSnapshotV1(
1628
+ await userConfigurationStub(env, userId).hasBot({
1629
+ schemaVersion: 1,
1630
+ userId,
1631
+ botId,
1632
+ }),
1633
+ ),
1634
+ );
1635
+ if (!membership.registered) throw new ComputerBotNotFoundError(botId);
1636
+ return botStateStub(env, userId, botId);
1637
+ }
1638
+
1639
+ function voiceAssistantStub(env: Env, userId: string) {
1640
+ return env.VOICE_ASSISTANTS.get(env.VOICE_ASSISTANTS.idFromName(userId));
1641
+ }
1642
+
1643
+ /**
1644
+ * The tidy-up the dictation relay offers a finished capture.
1645
+ *
1646
+ * Two jobs and no judgement: book the spend against the account's own voice
1647
+ * object, then reach the model gateway with the deployment's credential. What
1648
+ * is asked for and what is accepted back belong to the relay, which is where
1649
+ * they can be tested without a model.
1650
+ *
1651
+ * Absent — and so no tidying at all — in a deployment with no usable `AI`
1652
+ * binding. A draft that keeps the ums is a working draft.
1653
+ */
1654
+ function voiceDictationCleanup(
1655
+ env: Env,
1656
+ userId: string,
1657
+ ): VoiceDictationCleanupV1 | undefined {
1658
+ const ai = env.AI;
1659
+ if (!ai || typeof Reflect.get(ai, "gateway") !== "function") return undefined;
1660
+ return {
1661
+ run: async (body, signal) => {
1662
+ const stub = await getAgentByName(env.VOICE_ASSISTANTS, userId);
1663
+ const booked = await stub.dictationLease({
1664
+ schemaVersion: 1,
1665
+ userId,
1666
+ // The lease id is this booking's, not the capture's: nothing is held
1667
+ // and nothing is released, so it only has to satisfy the decoder.
1668
+ leaseId: crypto.randomUUID(),
1669
+ action: "cleanup",
1670
+ });
1671
+ if (booked.status !== "cleanup" || !booked.admitted) return undefined;
1672
+ const host = createFrockAiGatewayHostV1(ai as Pick<Ai, "gateway">, {
1673
+ gatewayId: frockAiWorkerVarV1(env, "FROCK_AI_GATEWAY_ID"),
1674
+ autoRoute: frockAiWorkerVarV1(env, "FROCK_AI_AUTO_ROUTE"),
1675
+ accountId: frockAiWorkerVarV1(env, "FROCK_AI_ACCOUNT_ID"),
1676
+ token: frockAiWorkerVarV1(env, "FROCK_AI_GATEWAY_TOKEN"),
1677
+ });
1678
+ const model =
1679
+ env.VOICE_DICTATION_CLEANUP_MODEL?.trim() ||
1680
+ VOICE_DICTATION_CLEANUP_MODEL_V1;
1681
+ const stream = await host.runChatCompletion(model, body, signal);
1682
+ return voiceDictationCleanupAnswerV1(await new Response(stream).text());
1683
+ },
1684
+ };
1685
+ }
1686
+
1687
+ /**
1688
+ * The assistant's message out of one unstreamed chat completion.
1689
+ *
1690
+ * Returns the empty string for a body that is not the shape we asked for,
1691
+ * which every guard downstream reads as "nothing came back" and answers by
1692
+ * keeping the raw transcript. A malformed answer must never be a thrown
1693
+ * error here: it is an ordinary outcome of asking a model for text.
1694
+ */
1695
+ export function voiceDictationCleanupAnswerV1(raw: string): string {
1696
+ try {
1697
+ const body = JSON.parse(raw) as {
1698
+ choices?: { message?: { content?: unknown } }[];
1699
+ };
1700
+ const content = body.choices?.[0]?.message?.content;
1701
+ return typeof content === "string" ? content : "";
1702
+ } catch {
1703
+ return "";
1704
+ }
1705
+ }
1706
+
1707
+ /** The voice doors the gateway opens once it has proved the identity. */
1708
+ function voiceGatewayDependencies(env: Env): VoiceGatewayDependencies {
1709
+ return {
1710
+ capabilities: () => ({
1711
+ dictation: voiceDictationConfiguredV1(env),
1712
+ assistant: voiceAssistantConfiguredV1(env),
1713
+ }),
1714
+ openDictation: async (userId, request) => {
1715
+ // The account's voice object holds the dictation lease: one capture
1716
+ // at a time and a booked window of seconds, decided before the provider
1717
+ // is opened. RPC goes through the Agent lifecycle; the assistant
1718
+ // socket below does not, so a cold start is paid once on the 101.
1719
+ const stub = await getAgentByName(env.VOICE_ASSISTANTS, userId);
1720
+ const leaseId = crypto.randomUUID();
1721
+ const call = (input: Record<string, unknown>) =>
1722
+ stub.dictationLease({ schemaVersion: 1, userId, leaseId, ...input });
1723
+ return openVoiceDictationRelayV1(request, {
1724
+ env,
1725
+ cleanup: voiceDictationCleanup(env, userId),
1726
+ cleanupJudge: createHostedDictationCleanupJudgeV1({
1727
+ JEV_API_KEY: env.JEV_API_KEY,
1728
+ }),
1729
+ lease: {
1730
+ acquire: async () => {
1731
+ const answer = await call({ action: "acquire" });
1732
+ return answer.status === "acquired"
1733
+ ? { status: "acquired" }
1734
+ : {
1735
+ status: "refused",
1736
+ reason:
1737
+ answer.status === "refused"
1738
+ ? answer.reason
1739
+ : "Dictation is unavailable right now.",
1740
+ };
1741
+ },
1742
+ renew: async () => {
1743
+ const answer = await call({ action: "renew" });
1744
+ return answer.status === "renewed" && answer.ok;
1745
+ },
1746
+ release: async (activeSeconds) => {
1747
+ await call({ action: "release", activeSeconds });
1748
+ },
1749
+ },
1750
+ });
1751
+ },
1752
+ openAssistant: async (userId, deviceKey, request, context) => {
1753
+ const incoming = new URL(request.url);
1754
+ const internal = new URL(
1755
+ `${VOICE_ASSISTANT_INTERNAL_PATH}${incoming.search}`,
1756
+ "https://voice-assistant.internal",
1757
+ );
1758
+ const headers = new Headers(request.headers);
1759
+ headers.delete(VOICE_ASSISTANT_USER_HEADER);
1760
+ headers.set(VOICE_ASSISTANT_USER_HEADER, userId);
1761
+ headers.set(VOICE_ASSISTANT_DEVICE_HEADER, deviceKey);
1762
+ headers.set("x-frockbot-auth-session-v1", context.authMode);
1763
+ headers.set("x-frockbot-is-admin-v1", String(context.isAdmin));
1764
+ // Named for the User and reached only through this door: there is no
1765
+ // `/agents/*` route, so an object nobody signed in as is never opened.
1766
+ // `getAgentByName` waits for `onStart` before returning the stub, which
1767
+ // would hold the 101 until ledger recovery finished. The fetch itself
1768
+ // starts the Agent; recovery runs in that same invocation.
1769
+ return voiceAssistantStub(env, userId).fetch(
1770
+ new Request(internal, { method: "GET", headers }),
1771
+ );
1772
+ },
1773
+ };
1774
+ }
1775
+
1776
+ async function openOwnedBotStateChannel(
1777
+ env: Env,
1778
+ userId: string,
1779
+ botId: string,
1780
+ request: Request,
1781
+ context: { isAdmin: boolean; authMode: string },
1782
+ ): Promise<Response> {
1783
+ // Prove directory membership before naming the object, exactly like every
1784
+ // Computer read and command route.
1785
+ await ownedComputerBotState(env, userId, botId);
1786
+ const incoming = new URL(request.url);
1787
+ const internal = new URL(
1788
+ `${BOT_STATE_CHANNEL_INTERNAL_PATH}${incoming.search}`,
1789
+ "https://bot-state.internal",
1790
+ );
1791
+ const headers = new Headers(request.headers);
1792
+ headers.delete("x-frockbot-user-id");
1793
+ headers.delete("x-frockbot-bot-id");
1794
+ headers.set("x-frockbot-user-id", userId);
1795
+ headers.set("x-frockbot-bot-id", botId);
1796
+ headers.set("x-frockbot-auth-session-v1", context.authMode);
1797
+ headers.set("x-frockbot-is-admin-v1", String(context.isAdmin));
1798
+ return botStateObject(env, userId, botId).fetch(
1799
+ new Request(internal, { method: "GET", headers }),
1800
+ );
1801
+ }
1802
+
1803
+ interface RuntimeExports {
1804
+ UserBotState(options: { props: UserScopedProps }): RpcBoundary<UserBotState>;
1805
+ }
1806
+
1807
+ /** The User object's Group Chat doors; each answers a value, never a class. */
1808
+ function groupChatUser(env: Env, userId: string) {
1809
+ // SAFETY: Wrangler binds USER_CONFIGURATIONS to UserConfiguration.
1810
+ return env.USER_CONFIGURATIONS.get(
1811
+ env.USER_CONFIGURATIONS.idFromName(userId),
1812
+ ) as unknown as {
1813
+ listGroupChats(input: unknown): Promise<unknown>;
1814
+ executeGroupChatCommand(input: unknown): Promise<unknown>;
1815
+ };
1816
+ }
1817
+
1818
+ /** A Group Chat's own object, addressed by the User and the group together. */
1819
+ function groupChatObject(env: Env, userId: string, groupId: string) {
1820
+ // SAFETY: Wrangler binds GROUP_CHATS to GroupChat; these are its RPC doors.
1821
+ return env.GROUP_CHATS.get(
1822
+ env.GROUP_CHATS.idFromName(groupChatObjectNameV1(userId, groupId)),
1823
+ ) as unknown as {
1824
+ view(input: unknown): Promise<unknown>;
1825
+ page(input: unknown): Promise<unknown>;
1826
+ postFromUser(input: unknown): Promise<unknown>;
1827
+ markRead(input: unknown): Promise<unknown>;
1828
+ stop(input: unknown): Promise<unknown>;
1829
+ retry(input: unknown): Promise<unknown>;
1830
+ fetch(request: Request): Promise<Response>;
1831
+ };
1832
+ }
1833
+
1834
+ const createGatewayBackendContributions = (env: Env) =>
1835
+ createFoundationBackendContributions({
1836
+ backendHost: "gateway",
1837
+ brand: BRAND_V1,
1838
+ listGroupChats: async (userId: string) =>
1839
+ unwrapGroupRpcV1<GroupChatListV1>(
1840
+ await groupChatUser(env, userId).listGroupChats({
1841
+ schemaVersion: 1,
1842
+ userId,
1843
+ }),
1844
+ ),
1845
+ executeGroupChatCommand: async (
1846
+ userId: string,
1847
+ command: GroupChatCommandV1,
1848
+ ) =>
1849
+ unwrapGroupRpcV1<GroupChatReceiptV1>(
1850
+ await groupChatUser(env, userId).executeGroupChatCommand({
1851
+ schemaVersion: 1,
1852
+ userId,
1853
+ command,
1854
+ }),
1855
+ ),
1856
+ readGroupChat: async (userId: string, groupId: string) =>
1857
+ unwrapGroupRpcV1<GroupChatViewV1>(
1858
+ await groupChatObject(env, userId, groupId).view({
1859
+ schemaVersion: 1,
1860
+ userId,
1861
+ groupId,
1862
+ }),
1863
+ ),
1864
+ readGroupMessages: async (
1865
+ userId: string,
1866
+ groupId: string,
1867
+ query: { before?: number; after?: number; limit: number },
1868
+ ) =>
1869
+ unwrapGroupRpcV1<GroupMessagePageV1>(
1870
+ await groupChatObject(env, userId, groupId).page({
1871
+ schemaVersion: 1,
1872
+ userId,
1873
+ groupId,
1874
+ ...query,
1875
+ }),
1876
+ ),
1877
+ postGroupMessage: async (
1878
+ userId: string,
1879
+ groupId: string,
1880
+ command: GroupPostCommandV1,
1881
+ ) =>
1882
+ unwrapGroupRpcV1<{ schemaVersion: 1; message: GroupMessageV1 }>(
1883
+ await groupChatObject(env, userId, groupId).postFromUser({
1884
+ schemaVersion: 1,
1885
+ userId,
1886
+ groupId,
1887
+ command,
1888
+ }),
1889
+ ),
1890
+ markGroupRead: async (
1891
+ userId: string,
1892
+ groupId: string,
1893
+ command: GroupReadCommandV1,
1894
+ ) =>
1895
+ unwrapGroupRpcV1<{ schemaVersion: 1; readThrough: number }>(
1896
+ await groupChatObject(env, userId, groupId).markRead({
1897
+ schemaVersion: 1,
1898
+ userId,
1899
+ groupId,
1900
+ upTo: command.upTo,
1901
+ }),
1902
+ ),
1903
+ stopGroupTurns: async (
1904
+ userId: string,
1905
+ groupId: string,
1906
+ command: GroupStopCommandV1,
1907
+ ) =>
1908
+ unwrapGroupRpcV1<{ schemaVersion: 1; stopped: string[] }>(
1909
+ await groupChatObject(env, userId, groupId).stop({
1910
+ schemaVersion: 1,
1911
+ userId,
1912
+ groupId,
1913
+ command,
1914
+ }),
1915
+ ),
1916
+ retryGroupTurn: async (
1917
+ userId: string,
1918
+ groupId: string,
1919
+ command: GroupRetryCommandV1,
1920
+ ) =>
1921
+ unwrapGroupRpcV1<{ schemaVersion: 1 }>(
1922
+ await groupChatObject(env, userId, groupId).retry({
1923
+ schemaVersion: 1,
1924
+ userId,
1925
+ groupId,
1926
+ command,
1927
+ }),
1928
+ ),
1929
+ openGroupChannel: (userId: string, groupId: string, request: Request) => {
1930
+ const headers = new Headers(request.headers);
1931
+ headers.set("x-frockbot-user-id", userId);
1932
+ headers.set("x-frockbot-group-id", groupId);
1933
+ return groupChatObject(env, userId, groupId).fetch(
1934
+ new Request(
1935
+ new URL(GROUP_CHANNEL_INTERNAL_PATH, "https://group-chat.internal"),
1936
+ { method: "GET", headers },
1937
+ ),
1938
+ );
1939
+ },
1940
+ listBots: async (userId) =>
1941
+ decodeDirectoryViewV1(
1942
+ rpcJsonSnapshotV1(
1943
+ await userConfigurationStub(env, userId).listBots({
1944
+ schemaVersion: 1,
1945
+ userId,
1946
+ }),
1947
+ ),
1948
+ ),
1949
+ readFlockBootstrap: async (userId: string) =>
1950
+ decodeFlockBootstrapViewV1(
1951
+ rpcJsonSnapshotV1(
1952
+ await userConfigurationStub(env, userId).readFlockBootstrap({
1953
+ schemaVersion: 1,
1954
+ userId,
1955
+ }),
1956
+ ),
1957
+ ),
1958
+ listBotLifecycles: async (userId: string) =>
1959
+ decodeBotLifecycleDirectoryViewV1(
1960
+ rpcJsonSnapshotV1(
1961
+ await userConfigurationStub(env, userId).listBotLifecycles({
1962
+ schemaVersion: 1,
1963
+ userId,
1964
+ }),
1965
+ ),
1966
+ ),
1967
+ executeBotLifecycle: async (
1968
+ userId: string,
1969
+ command: BotLifecycleCommandV1,
1970
+ ) =>
1971
+ decodeBotLifecycleReceiptV1(
1972
+ rpcJsonSnapshotV1(
1973
+ await userConfigurationStub(env, userId).executeBotLifecycle({
1974
+ schemaVersion: 1,
1975
+ userId,
1976
+ command,
1977
+ }),
1978
+ ),
1979
+ ),
1980
+ createBot: async (userId, command) =>
1981
+ decodeFlockReceiptV1(
1982
+ rpcJsonSnapshotV1(
1983
+ await userConfigurationStub(env, userId).createBot({
1984
+ schemaVersion: 1,
1985
+ userId,
1986
+ command,
1987
+ }),
1988
+ ),
1989
+ ),
1990
+ listBotIdentities: (userId: string) => listBotIdentities(env, userId),
1991
+ readComputer: async (userId: string, botId: string) =>
1992
+ decodeComputerProjectionV1(
1993
+ rpcJsonSnapshotV1(
1994
+ await (
1995
+ await ownedComputerBotState(env, userId, botId)
1996
+ ).readComputerPresence(),
1997
+ ),
1998
+ ),
1999
+ readComputerFrame: async (
2000
+ userId: string,
2001
+ botId: string,
2002
+ contentHash: string,
2003
+ ) => {
2004
+ const answer = await (
2005
+ await ownedComputerBotState(env, userId, botId)
2006
+ ).readComputerFrame(contentHash);
2007
+ if (!answer) return undefined;
2008
+ const binary = atob(answer.bytesBase64);
2009
+ const bytes = new Uint8Array(binary.length);
2010
+ for (let index = 0; index < binary.length; index += 1) {
2011
+ bytes[index] = binary.charCodeAt(index);
2012
+ }
2013
+ return bytes;
2014
+ },
2015
+ executeComputerCommand: async (
2016
+ userId: string,
2017
+ botId: string,
2018
+ command: ComputerCommandV1,
2019
+ ) =>
2020
+ decodeComputerCommandResponse(
2021
+ rpcJsonSnapshotV1(
2022
+ await (
2023
+ await ownedComputerBotState(env, userId, botId)
2024
+ ).executeComputerPresenceCommand(command),
2025
+ ),
2026
+ ),
2027
+ searchTranscripts: async (userId: string, query: SearchQueryV1) =>
2028
+ decodeSearchIndexResultsV1(
2029
+ rpcJsonSnapshotV1(
2030
+ await userSearchStub(env, userId).searchTranscripts({
2031
+ schemaVersion: 1,
2032
+ userId,
2033
+ query,
2034
+ }),
2035
+ ),
2036
+ ),
2037
+ rebuildSearchIndex: async (userId: string) =>
2038
+ decodeClientSearchRebuildReceiptV1(
2039
+ rpcJsonSnapshotV1(
2040
+ await userSearchStub(env, userId).rebuildSearchIndex({
2041
+ schemaVersion: 1,
2042
+ userId,
2043
+ }),
2044
+ ),
2045
+ ),
2046
+ readAudit: async (userId: string, query: AuditQueryV1) =>
2047
+ decodeClientAuditPageV1(
2048
+ rpcJsonSnapshotV1(
2049
+ await userAuditStub(env, userId).readAuditEntries({
2050
+ schemaVersion: 1,
2051
+ userId,
2052
+ ...(query.botId === undefined ? {} : { botId: query.botId }),
2053
+ ...(query.kind === undefined ? {} : { kind: query.kind }),
2054
+ ...(query.target === undefined ? {} : { target: query.target }),
2055
+ ...(query.before === undefined ? {} : { before: query.before }),
2056
+ ...(query.limit === undefined ? {} : { limit: query.limit }),
2057
+ }),
2058
+ ),
2059
+ ),
2060
+ readActivity: async (userId: string, query: AuditActivityQueryV1) =>
2061
+ decodeAuditActivityPageV1(
2062
+ rpcJsonSnapshotV1(
2063
+ await userAuditStub(env, userId).readAuditActivity({
2064
+ schemaVersion: 1,
2065
+ userId,
2066
+ ...(query.botId === undefined ? {} : { botId: query.botId }),
2067
+ ...(query.filter === undefined ? {} : { filter: query.filter }),
2068
+ ...(query.before === undefined ? {} : { before: query.before }),
2069
+ ...(query.limit === undefined ? {} : { limit: query.limit }),
2070
+ }),
2071
+ ),
2072
+ ),
2073
+ rebuildAuditIndex: async (userId: string) =>
2074
+ decodeAuditRebuildReceiptV1(
2075
+ rpcJsonSnapshotV1(
2076
+ await userAuditStub(env, userId).rebuildAuditIndex({
2077
+ schemaVersion: 1,
2078
+ userId,
2079
+ }),
2080
+ ),
2081
+ ),
2082
+ listBotUnread: (userId: string) => listBotUnread(env, userId),
2083
+ listBotNotifications: (userId: string) => listBotNotifications(env, userId),
2084
+ executeBotUnreadCommand: (
2085
+ userId: string,
2086
+ botId: string,
2087
+ command: BotUnreadCommandV1,
2088
+ ) => executeBotUnreadCommand(env, userId, botId, command),
2089
+ readAvatar: async (userId, botId) =>
2090
+ decodeAvatarIdentityViewV1(
2091
+ rpcJsonSnapshotV1(
2092
+ await botStateStub(env, userId, botId).readAvatar({
2093
+ schemaVersion: 1,
2094
+ userId,
2095
+ botId,
2096
+ }),
2097
+ ),
2098
+ ),
2099
+ readVoice: async (userId, botId) =>
2100
+ decodeVoiceIdentityViewV1(
2101
+ rpcJsonSnapshotV1(
2102
+ await botStateStub(env, userId, botId).readVoice({
2103
+ schemaVersion: 1,
2104
+ userId,
2105
+ botId,
2106
+ }),
2107
+ ),
2108
+ ),
2109
+ readLook: async (userId, botId) =>
2110
+ decodeLookIdentityViewV1(
2111
+ rpcJsonSnapshotV1(
2112
+ await botStateStub(env, userId, botId).readLook({
2113
+ schemaVersion: 1,
2114
+ userId,
2115
+ botId,
2116
+ }),
2117
+ ),
2118
+ ),
2119
+ executeConnection: (userId, command) =>
2120
+ userConfigurationStub(env, userId).executeConnection({
2121
+ schemaVersion: 1,
2122
+ userId,
2123
+ command,
2124
+ }),
2125
+ listConnectTriggers: async (userId) => {
2126
+ const offers = await userConfigurationStub(
2127
+ env,
2128
+ userId,
2129
+ ).listConnectTriggers({
2130
+ schemaVersion: 1,
2131
+ userId,
2132
+ });
2133
+ return Array.isArray(offers) ? offers : [];
2134
+ },
2135
+ ...(typeof env.COMPOSIO_WEBHOOK_SECRET === "string"
2136
+ ? { connectWebhookSecret: env.COMPOSIO_WEBHOOK_SECRET }
2137
+ : {}),
2138
+ // The User object signs a sign-in's state under the keyring; the
2139
+ // callback verifies it here before any object is addressed.
2140
+ ...(typeof env.CREDENTIAL_KEYRING === "string"
2141
+ ? { mcpSignInKeyring: env.CREDENTIAL_KEYRING }
2142
+ : {}),
2143
+ handleConnectEvent: (input) =>
2144
+ userConfigurationStub(env, input.userId).handleConnectEvent({
2145
+ schemaVersion: 1,
2146
+ userId: input.userId,
2147
+ event: input.event,
2148
+ }),
2149
+ lookupConnectionCommand: (userId, packageId, commandId) =>
2150
+ userConfigurationStub(env, userId).lookupConnectionCommand({
2151
+ schemaVersion: 1,
2152
+ userId,
2153
+ packageId,
2154
+ commandId,
2155
+ }),
2156
+ listCompositionGenerations: async (userId, botId, query) =>
2157
+ decodeCompositionGenerationListViewV1(
2158
+ await botStateStub(env, userId, botId).listCompositionGenerations({
2159
+ schemaVersion: 1,
2160
+ userId,
2161
+ botId,
2162
+ query,
2163
+ }),
2164
+ ),
2165
+ getCompositionGeneration: async (userId, botId, generationId) => {
2166
+ const generation = await botStateStub(
2167
+ env,
2168
+ userId,
2169
+ botId,
2170
+ ).getCompositionGeneration({
2171
+ schemaVersion: 1,
2172
+ userId,
2173
+ botId,
2174
+ generationId,
2175
+ });
2176
+ return generation === undefined
2177
+ ? undefined
2178
+ : decodeCompositionGenerationViewV1(generation);
2179
+ },
2180
+ listTasks: async (userId, botId) =>
2181
+ // Snapshotted first: a cross-object answer arrives as a live stub
2182
+ // carrying `Symbol.dispose`, and an exact-keys decoder is right to
2183
+ // refuse that.
2184
+ decodeTaskListViewV1(
2185
+ rpcJsonSnapshotV1(
2186
+ await botStateStub(env, userId, botId).listTasks({
2187
+ schemaVersion: 1,
2188
+ userId,
2189
+ botId,
2190
+ }),
2191
+ ),
2192
+ ),
2193
+ readTask: async (userId, botId, taskId) =>
2194
+ decodeTaskViewV1(
2195
+ rpcJsonSnapshotV1(
2196
+ await botStateStub(env, userId, botId).readTask({
2197
+ schemaVersion: 1,
2198
+ userId,
2199
+ botId,
2200
+ taskId,
2201
+ }),
2202
+ ),
2203
+ ),
2204
+ stopTask: async (userId, botId, taskId) =>
2205
+ decodeTaskViewV1(
2206
+ rpcJsonSnapshotV1(
2207
+ await botStateStub(env, userId, botId).stopTask({
2208
+ schemaVersion: 1,
2209
+ userId,
2210
+ botId,
2211
+ taskId,
2212
+ }),
2213
+ ),
2214
+ ),
2215
+ listRoutines: async (userId, botId) =>
2216
+ decodeRoutineListViewV1(
2217
+ await botStateStub(env, userId, botId).listRoutines({
2218
+ schemaVersion: 1,
2219
+ userId,
2220
+ botId,
2221
+ }),
2222
+ ),
2223
+ readRoutinesFrame: async (userId, botId) => {
2224
+ const frame = await botStateStub(env, userId, botId).readRoutinesFrame({
2225
+ schemaVersion: 1,
2226
+ userId,
2227
+ botId,
2228
+ });
2229
+ return {
2230
+ list: decodeRoutineListViewV1(frame.list),
2231
+ inbox: decodeRoutineInboxViewV1(frame.inbox),
2232
+ };
2233
+ },
2234
+ executeRoutineCommand: async (userId, botId, command) =>
2235
+ decodeRoutineCommandReceiptV1(
2236
+ await botStateStub(env, userId, botId).executeRoutineCommand({
2237
+ schemaVersion: 1,
2238
+ userId,
2239
+ botId,
2240
+ command,
2241
+ }),
2242
+ ),
2243
+ // The secret the gateway verifies a presented machine token or pairing
2244
+ // code against, before any Durable Object is addressed. It never leaves
2245
+ // the Worker: what crosses to the User object is the token's claims and
2246
+ // its digest, never the token.
2247
+ ...(typeof env.MACHINE_TOKEN_SECRET === "string"
2248
+ ? { machineTokenSecret: env.MACHINE_TOKEN_SECRET }
2249
+ : {}),
2250
+ createMachinePairing: async (userId) =>
2251
+ decodeMachinePairingOfferV1(
2252
+ rpcJsonSnapshotV1(
2253
+ await userMachineStub(env, userId).createMachinePairing({
2254
+ schemaVersion: 1,
2255
+ userId,
2256
+ }),
2257
+ ),
2258
+ ),
2259
+ enrollMachine: async (userId, input) => {
2260
+ const refusal = await externalAccountRefusal(env, userId);
2261
+ if (refusal) throw new MachineTokenError(refusal.status, refusal.message);
2262
+ return decodeMachineEnrollmentReceiptV1(
2263
+ rpcJsonSnapshotV1(
2264
+ await userMachineStub(env, userId).enrollMachine({
2265
+ schemaVersion: 1,
2266
+ userId,
2267
+ machineId: input.machineId,
2268
+ enrollment: input.enrollment,
2269
+ }),
2270
+ ),
2271
+ );
2272
+ },
2273
+ // An upgrade cannot cross RPC, so the socket goes to the object's `fetch`.
2274
+ openMachineSocket: (userId, call, request) =>
2275
+ env.USER_CONFIGURATIONS.get(
2276
+ env.USER_CONFIGURATIONS.idFromName(userId),
2277
+ ).fetch(internalMachineSocketRequestV1(userId, call, request)),
2278
+ claimMachineCommand: async (userId, call) =>
2279
+ decodeMachineClaimReceiptV1(
2280
+ rpcJsonSnapshotV1(
2281
+ await userMachineStub(env, userId).claimMachineCommand({
2282
+ schemaVersion: 1,
2283
+ userId,
2284
+ machineId: call.machineId,
2285
+ commandId: call.commandId,
2286
+ claims: call.claims,
2287
+ tokenDigest: call.tokenDigest,
2288
+ }),
2289
+ ),
2290
+ ),
2291
+ recordMachineResult: async (userId, call) => {
2292
+ const receipt = decodeMachineResultReceiptV1(
2293
+ rpcJsonSnapshotV1(
2294
+ await userMachineStub(env, userId).recordMachineResult({
2295
+ schemaVersion: 1,
2296
+ userId,
2297
+ machineId: call.machineId,
2298
+ commandId: call.commandId,
2299
+ claims: call.claims,
2300
+ tokenDigest: call.tokenDigest,
2301
+ result: call.result,
2302
+ }),
2303
+ ),
2304
+ );
2305
+ // The Bot that asked is told here rather than by the User Durable
2306
+ // Object: a Durable Object holding a live reference to another one
2307
+ // cannot be evicted while it does, and this registry is built to
2308
+ // depend on neither presence nor residency. The outbox is durable, so
2309
+ // the hand-off is not lost by being made from out here.
2310
+ await deliverMachineResults(env, userId);
2311
+ return receipt;
2312
+ },
2313
+ loadMachineModule: async (userId, call) => {
2314
+ const { found } = await userMachineStub(env, userId).readMachineModule({
2315
+ schemaVersion: 1,
2316
+ userId,
2317
+ machineId: call.machineId,
2318
+ contentHash: call.contentHash,
2319
+ claims: call.claims,
2320
+ tokenDigest: call.tokenDigest,
2321
+ });
2322
+ if (!found) return undefined;
2323
+ const object = await env.APPLICATION_ARTIFACTS.get(
2324
+ pluginModuleKeyV1(call.contentHash),
2325
+ );
2326
+ return object ? await object.arrayBuffer() : undefined;
2327
+ },
2328
+ recordMachineModuleReports: async (userId, call) =>
2329
+ decodeMachineModuleReportsReceiptV1(
2330
+ rpcJsonSnapshotV1(
2331
+ await userMachineStub(env, userId).recordMachineModuleReports({
2332
+ schemaVersion: 1,
2333
+ userId,
2334
+ machineId: call.machineId,
2335
+ claims: call.claims,
2336
+ tokenDigest: call.tokenDigest,
2337
+ reports: call.reports,
2338
+ }),
2339
+ ),
2340
+ ),
2341
+ claimMachineModuleCall: async (userId, call) =>
2342
+ decodeMachineModuleCallClaimReceiptV1(
2343
+ rpcJsonSnapshotV1(
2344
+ await userMachineStub(env, userId).claimMachineModuleCall({
2345
+ schemaVersion: 1,
2346
+ userId,
2347
+ machineId: call.machineId,
2348
+ callId: call.callId,
2349
+ claims: call.claims,
2350
+ tokenDigest: call.tokenDigest,
2351
+ }),
2352
+ ),
2353
+ ),
2354
+ recordMachineModuleCallResult: async (userId, call) =>
2355
+ decodeMachineModuleCallResultReceiptV1(
2356
+ rpcJsonSnapshotV1(
2357
+ await userMachineStub(env, userId).recordMachineModuleCallResult({
2358
+ schemaVersion: 1,
2359
+ userId,
2360
+ machineId: call.machineId,
2361
+ callId: call.callId,
2362
+ claims: call.claims,
2363
+ tokenDigest: call.tokenDigest,
2364
+ result: call.result,
2365
+ }),
2366
+ ),
2367
+ ),
2368
+ recordMachineModuleEvents: async (userId, call) =>
2369
+ decodeMachineModuleEventsReceiptV1(
2370
+ rpcJsonSnapshotV1(
2371
+ await userMachineStub(env, userId).recordMachineModuleEvents({
2372
+ schemaVersion: 1,
2373
+ userId,
2374
+ machineId: call.machineId,
2375
+ claims: call.claims,
2376
+ tokenDigest: call.tokenDigest,
2377
+ events: call.events,
2378
+ }),
2379
+ ),
2380
+ ),
2381
+ listMachines: async (userId) =>
2382
+ decodeMachineListViewV1(
2383
+ rpcJsonSnapshotV1(
2384
+ await userMachineStub(env, userId).listMachines({
2385
+ schemaVersion: 1,
2386
+ userId,
2387
+ }),
2388
+ ),
2389
+ ),
2390
+ revokeMachine: async (userId, machineId) =>
2391
+ decodeMachineListViewV1(
2392
+ rpcJsonSnapshotV1(
2393
+ await userMachineStub(env, userId).revokeMachine({
2394
+ schemaVersion: 1,
2395
+ userId,
2396
+ machineId,
2397
+ }),
2398
+ ),
2399
+ ),
2400
+ // The account's username and senders, and each Bot's switch. The messages
2401
+ // themselves arrive at `email()` below, never through the gateway.
2402
+ ...(() => {
2403
+ const domain = emailDomainV1(env);
2404
+ return domain ? { emailDomain: domain } : {};
2405
+ })(),
2406
+ emailSignIn: (userId) => emailSignInV1(env, userId),
2407
+ readInboundEmail: async (userId, botId) =>
2408
+ decodeInboundEmailStateV1(
2409
+ rpcJsonSnapshotV1(
2410
+ await userInboundEmailStub(env, userId).readInboundEmail({
2411
+ schemaVersion: 1,
2412
+ userId,
2413
+ botId,
2414
+ }),
2415
+ ),
2416
+ ),
2417
+ readEmailUsername: async (userId) => {
2418
+ const { username } = rpcJsonSnapshotV1(
2419
+ await deploymentPolicyStub(env).readEmailUsername({
2420
+ schemaVersion: 1,
2421
+ userId,
2422
+ }),
2423
+ ) as { username?: unknown };
2424
+ return typeof username === "string" ? username : undefined;
2425
+ },
2426
+ claimEmailUsername: async (userId, username) => {
2427
+ if (username === undefined) {
2428
+ await deploymentPolicyStub(env).releaseEmailUsername({
2429
+ schemaVersion: 1,
2430
+ userId,
2431
+ });
2432
+ return { status: "claimed" };
2433
+ }
2434
+ const { status } = rpcJsonSnapshotV1(
2435
+ await deploymentPolicyStub(env).claimEmailUsername({
2436
+ schemaVersion: 1,
2437
+ userId,
2438
+ username,
2439
+ }),
2440
+ ) as { status?: unknown };
2441
+ return { status: status === "claimed" ? "claimed" : "taken" };
2442
+ },
2443
+ setBotEmailEnabled: async (userId, botId, enabled) =>
2444
+ inboundEmailCommandOutcomeV1(
2445
+ await userInboundEmailStub(env, userId).setBotEmailEnabled({
2446
+ schemaVersion: 1,
2447
+ userId,
2448
+ botId,
2449
+ enabled,
2450
+ }),
2451
+ ),
2452
+ commandInboundEmailSender: async (userId, command) =>
2453
+ inboundEmailCommandOutcomeV1(
2454
+ await userInboundEmailStub(env, userId).commandInboundEmailSender({
2455
+ schemaVersion: 1,
2456
+ userId,
2457
+ ...command,
2458
+ }),
2459
+ ),
2460
+ // The secret the gateway verifies a presented webhook key against. It
2461
+ // never leaves the Worker; a Bot only ever sees a digest.
2462
+ ...(typeof env.ROUTINE_HOOK_SECRET === "string"
2463
+ ? { routineHookSecret: env.ROUTINE_HOOK_SECRET }
2464
+ : {}),
2465
+ deliverRoutineHook: async (userId, botId, delivery) => {
2466
+ const refusal = await externalAccountRefusal(env, userId);
2467
+ if (refusal) throw new RoutineHookError(refusal.status, refusal.message);
2468
+ return botStateStub(env, userId, botId).deliverRoutineHook({
2469
+ schemaVersion: 1,
2470
+ userId,
2471
+ botId,
2472
+ delivery,
2473
+ });
2474
+ },
2475
+ listRoutineRuns: async (userId, botId, routineId) =>
2476
+ decodeRoutineRunListViewV1(
2477
+ await botStateStub(env, userId, botId).listRoutineRuns({
2478
+ schemaVersion: 1,
2479
+ userId,
2480
+ botId,
2481
+ routineId,
2482
+ }),
2483
+ ),
2484
+ readRoutineRun: async (userId, botId, routineId, runId) =>
2485
+ decodeRoutineRunDetailViewV1(
2486
+ await botStateStub(env, userId, botId).readRoutineRun({
2487
+ schemaVersion: 1,
2488
+ userId,
2489
+ botId,
2490
+ routineId,
2491
+ runId,
2492
+ }),
2493
+ ),
2494
+ listRoutineInbox: async (userId, botId) =>
2495
+ decodeRoutineInboxViewV1(
2496
+ await botStateStub(env, userId, botId).listRoutineInbox({
2497
+ schemaVersion: 1,
2498
+ userId,
2499
+ botId,
2500
+ }),
2501
+ ),
2502
+ executeRoutineInboxCommand: async (userId, botId, command) =>
2503
+ decodeRoutineInboxReceiptV1(
2504
+ await botStateStub(env, userId, botId).executeRoutineInboxCommand({
2505
+ schemaVersion: 1,
2506
+ userId,
2507
+ botId,
2508
+ command,
2509
+ }),
2510
+ ),
2511
+ revertComposition: async (userId, botId, command) =>
2512
+ decodeCompositionCommandReceiptV1(
2513
+ await botStateStub(env, userId, botId).revertComposition({
2514
+ schemaVersion: 1,
2515
+ userId,
2516
+ botId,
2517
+ command,
2518
+ }),
2519
+ ),
2520
+ // Through the User rather than the Bot: the Bot applies the change and
2521
+ // the User's directory, which every Bot list reads, is told of it.
2522
+ updateAvatar: async (userId, botId, command) =>
2523
+ decodeFlockReceiptV1(
2524
+ rpcJsonSnapshotV1(
2525
+ await userConfigurationStub(env, userId).updateBotAvatar({
2526
+ schemaVersion: 1,
2527
+ userId,
2528
+ botId,
2529
+ command,
2530
+ }),
2531
+ ),
2532
+ ),
2533
+ updateVoice: async (userId, botId, command) =>
2534
+ decodeFlockReceiptV1(
2535
+ rpcJsonSnapshotV1(
2536
+ await userConfigurationStub(env, userId).updateBotVoice({
2537
+ schemaVersion: 1,
2538
+ userId,
2539
+ botId,
2540
+ command,
2541
+ }),
2542
+ ),
2543
+ ),
2544
+ updateLook: async (userId, botId, command) =>
2545
+ decodeFlockReceiptV1(
2546
+ rpcJsonSnapshotV1(
2547
+ await userConfigurationStub(env, userId).updateBotLook({
2548
+ schemaVersion: 1,
2549
+ userId,
2550
+ botId,
2551
+ command,
2552
+ }),
2553
+ ),
2554
+ ),
2555
+ });
2556
+
2557
+ /**
2558
+ * The last net under every entry point, for the rejections no boundary caught.
2559
+ *
2560
+ * `answeredEntryV1`/`loggedEntryV1` cover the entry points we know about, and a
2561
+ * detached promise is by definition one nobody is awaiting — so an isolate that
2562
+ * loses one has nowhere to report it. In the dev Worker that is not a quiet
2563
+ * loss: a run of uncaught rejections (`BotNotFoundError`, a refused Turn, a
2564
+ * provider that offers no retrieval) preceded wrangler exiting and taking the
2565
+ * shared stack down mid-test for every agent using it.
2566
+ *
2567
+ * Logging is all this does. It cannot make the rejection safe — the code that
2568
+ * produced it is what has to — but it names the failure that would otherwise
2569
+ * only ever appear as `exceptionId, url 'undefined'`, and it keeps the isolate
2570
+ * from treating the rejection as fatal.
2571
+ *
2572
+ * Registration is guarded because the event is a host capability, not a
2573
+ * language one: a runtime that does not offer it must not fail to boot.
2574
+ */
2575
+ try {
2576
+ addEventListener("unhandledrejection", (event: Event) => {
2577
+ const reason = (event as { reason?: unknown }).reason;
2578
+ console.error(
2579
+ `Unhandled rejection escaped an entry point: ${
2580
+ reason instanceof Error
2581
+ ? (reason.stack ?? reason.message)
2582
+ : String(reason)
2583
+ }`,
2584
+ );
2585
+ (event as { preventDefault?: () => void }).preventDefault?.();
2586
+ });
2587
+ } catch {
2588
+ // A runtime without the event still runs; it just reports less.
2589
+ }
2590
+
2591
+ export default {
2592
+ /**
2593
+ * One message from Email Routing, for the deployment's inbound domain. A
2594
+ * refusal is a permanent SMTP rejection; a failure to reach durable state
2595
+ * throws, and Email Routing delivers the message again.
2596
+ */
2597
+ async email(message: ForwardableEmailMessage, env: Env) {
2598
+ try {
2599
+ const outcome = await receiveInboundEmailV1(
2600
+ message,
2601
+ inboundEmailHostV1(env),
2602
+ );
2603
+ // The outcome and nothing about who: no address reaches the log.
2604
+ console.log(
2605
+ JSON.stringify({
2606
+ event: "inbound-email",
2607
+ status: outcome.status,
2608
+ ...(outcome.status === "rejected" ? { code: outcome.code } : {}),
2609
+ }),
2610
+ );
2611
+ } catch (error) {
2612
+ const name =
2613
+ typeof error === "object" && error !== null && "name" in error
2614
+ ? String(error.name)
2615
+ : "unknown";
2616
+ // Nothing the account held will read this, and no redelivery changes it.
2617
+ if (name === "AccountDeletedError") {
2618
+ message.setReject("This address does not accept mail.");
2619
+ return;
2620
+ }
2621
+ console.error(
2622
+ JSON.stringify({ event: "inbound-email-failed", error: name }),
2623
+ );
2624
+ throw error;
2625
+ }
2626
+ },
2627
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
2628
+ // The Computer's connected-account proxy: authorized by its own signed
2629
+ // token, before sessions, admission or any backend is mounted.
2630
+ if (new URL(request.url).pathname === COMPUTER_EGRESS_PATH_V1) {
2631
+ return routeComputerEgressV1(request, {
2632
+ ...(env.COMPUTER_HOST_TOKEN ? { secret: env.COMPUTER_HOST_TOKEN } : {}),
2633
+ answer: (input) => {
2634
+ // SAFETY: Wrangler binds BOT_STATES to BotState; workers-types cannot infer its generated RPC surface.
2635
+ const rpc = env.BOT_STATES.get(
2636
+ env.BOT_STATES.idFromName(input.object),
2637
+ ) as unknown as RpcBoundary<{
2638
+ answerComputerEgress(input: unknown): Promise<unknown>;
2639
+ }>;
2640
+ return rpc.answerComputerEgress(input);
2641
+ },
2642
+ });
2643
+ }
2644
+ let mountedBackend:
2645
+ Awaited<ReturnType<typeof createGatewayBackendContributions>> | undefined;
2646
+ // Opt-in voice diagnostics for the assistant upgrade and nothing else.
2647
+ // The entry is the first thing that happens to this request in this
2648
+ // Worker; everything the voice object can see starts long after it.
2649
+ const timing = voiceAssistantEdgeTimingOfV1(request.url);
2650
+ timing?.mark("edge-fetch");
2651
+ try {
2652
+ // SAFETY: exported WorkerEntrypoints are materialized on ctx.exports;
2653
+ // workers-types cannot infer the generated local RPC stubs.
2654
+ const runtimeExports = ctx.exports as unknown as RuntimeExports;
2655
+ mountedBackend = await createGatewayBackendContributions(env);
2656
+ timing?.mark("edge-backend-ready");
2657
+ // Which `env` secret the native door signs with belongs to the auth
2658
+ // Package: the hosted build keeps signing with the live
2659
+ // `BETTER_AUTH_SECRET`, and a build without better-auth has its own key.
2660
+ const nativeTokenSecret = AUTH_PACKAGE_V1.nativeTokenSecret.read(env);
2661
+ const gateway = createGateway(
2662
+ {
2663
+ loader: env.USER_APPLICATIONS,
2664
+ artifacts: new R2ApplicationArtifacts(env.APPLICATION_ARTIFACTS),
2665
+ registerPush: (userId, registration) =>
2666
+ env.USER_CONFIGURATIONS.get(
2667
+ env.USER_CONFIGURATIONS.idFromName(userId),
2668
+ ).registerPush({ userId, registration }),
2669
+ deletion: {
2670
+ deleteAccount: async (userId, command) =>
2671
+ rpcJsonSnapshotV1(
2672
+ await env.USER_CONFIGURATIONS.get(
2673
+ env.USER_CONFIGURATIONS.idFromName(userId),
2674
+ ).beginAccountDeletion({
2675
+ schemaVersion: 1,
2676
+ userId,
2677
+ ...command,
2678
+ }),
2679
+ ),
2680
+ deleteComputer: async (userId, commandId) =>
2681
+ rpcJsonSnapshotV1(
2682
+ await env.USER_CONFIGURATIONS.get(
2683
+ env.USER_CONFIGURATIONS.idFromName(userId),
2684
+ ).deleteComputer({ schemaVersion: 1, userId, commandId }),
2685
+ ),
2686
+ },
2687
+ auth: AUTH_PACKAGE_V1.create(env, {
2688
+ productName: BRAND_V1.productName,
2689
+ mayCreateIdentity: (candidate) => mayCreateIdentity(env, candidate),
2690
+ }),
2691
+ // The deployment's own origin, which is what `BETTER_AUTH_URL` is: a
2692
+ // development stack points it at its own host — the emulator reaches
2693
+ // this machine as 10.0.2.2, never as the hosted origin — and a
2694
+ // deployment that names none offers no native sign-in.
2695
+ ...(env.BETTER_AUTH_URL &&
2696
+ nativeReturnUrisFor(env, env.BETTER_AUTH_URL).length > 0 &&
2697
+ nativeTokenSecret
2698
+ ? {
2699
+ nativeAuth: createNativeAuth({
2700
+ secret: nativeTokenSecret,
2701
+ auth: AUTH_PACKAGE_V1.create(env, {
2702
+ productName: BRAND_V1.productName,
2703
+ mayCreateIdentity: (candidate) =>
2704
+ mayCreateIdentity(env, candidate),
2705
+ }),
2706
+ returnUris: nativeReturnUrisFor(env, env.BETTER_AUTH_URL),
2707
+ nativeApps: nativeAppsV1(env.NATIVE_APPS),
2708
+ brand: BRAND_V1,
2709
+ origin: env.BETTER_AUTH_URL,
2710
+ // The development door signs the app in as the development
2711
+ // identity in place of Google.
2712
+ ...(developmentAuthAllowed(env)
2713
+ ? { developmentUserId: DEVELOPMENT_USER_ID }
2714
+ : {}),
2715
+ admit: async (userId) => {
2716
+ // The stored identity, not anything the bearer carries: the
2717
+ // email an invitation binds to and the admin allowlist reads
2718
+ // are the identity provider's.
2719
+ return admitStoredAccount(env, userId);
2720
+ },
2721
+ session: async (userId, operation) => {
2722
+ const stub = env.USER_CONFIGURATIONS.get(
2723
+ env.USER_CONFIGURATIONS.idFromName(userId),
2724
+ );
2725
+ // SAFETY: this binding names UserConfiguration; this is its reviewed RPC.
2726
+ const rpc = stub as unknown as Pick<
2727
+ UserConfiguration,
2728
+ "nativeSession"
2729
+ >;
2730
+ const result = await rpc.nativeSession(operation);
2731
+ if (result.schemaVersion !== 1 || result.status !== "ok")
2732
+ throw new Error("Sign-in was refused");
2733
+ return result.record;
2734
+ },
2735
+ }),
2736
+ }
2737
+ : {}),
2738
+ admitAccount: (identity) => admitAccount(env, identity),
2739
+ ...(env.FROCKBOT_ADMIN_EMAILS
2740
+ ? { adminEmails: env.FROCKBOT_ADMIN_EMAILS }
2741
+ : {}),
2742
+ applicationHashFor: async () => env.DEFAULT_APPLICATION_HASH,
2743
+ botStateFor: (userId) =>
2744
+ runtimeExports.UserBotState({ props: { userId } }),
2745
+ userConfigurationFor: (userId): UserConfigurationBinding =>
2746
+ userConfigurationStub(env, userId),
2747
+ botConfigurationFor: (userId, botId): BotConfigurationBinding =>
2748
+ botStateStub(env, userId, botId),
2749
+ openBotStateChannel: (userId, botId, request, context) =>
2750
+ openOwnedBotStateChannel(env, userId, botId, request, context),
2751
+ voice: voiceGatewayDependencies(env),
2752
+ backendContributions: [
2753
+ ...mountedBackend.contributions,
2754
+ uploadRoutes(uploadRouteDependenciesV1(env)),
2755
+ billingRoutes(
2756
+ env,
2757
+ (userId) =>
2758
+ env.USER_CONFIGURATIONS.get(
2759
+ env.USER_CONFIGURATIONS.idFromName(userId),
2760
+ ) as unknown as BillingAccountRpc,
2761
+ async () =>
2762
+ decodeHostedModelRatesV1(
2763
+ rpcJsonSnapshotV1(
2764
+ await deploymentPolicyStub(env).readModelRates({
2765
+ schemaVersion: 1,
2766
+ }),
2767
+ ),
2768
+ ),
2769
+ ),
2770
+ ],
2771
+ debug: debugSurface(env),
2772
+ allowedClientOrigins: allowedClientOrigins(env),
2773
+ allowDevelopmentIdentity: env.ALLOW_DEVELOPMENT_AUTH === "true",
2774
+ whatsNew: BRAND_V1.whatsNew,
2775
+ },
2776
+ timing,
2777
+ );
2778
+ const answer = await gateway(request);
2779
+ timing?.mark("edge-answered", { status: answer.status });
2780
+ return answer;
2781
+ } catch (error) {
2782
+ // The outermost boundary. Building the gateway can fail before any route
2783
+ // runs — an unavailable binding, a Contribution that will not mount — and
2784
+ // workerd would answer that as plain text, which the client can only
2785
+ // report as a JSON parse error. Every answer this Worker gives is JSON
2786
+ // carrying a reason.
2787
+ return Response.json(
2788
+ {
2789
+ error:
2790
+ error instanceof Error ? error.message : "gateway request failed",
2791
+ },
2792
+ { status: 500 },
2793
+ );
2794
+ } finally {
2795
+ await mountedBackend?.dispose();
2796
+ }
2797
+ },
2798
+ } satisfies ExportedHandler<Env>;