@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.
- package/build-artifact.ts +130 -0
- package/build-flutter-web.ts +508 -0
- package/deployment-config/README.md +414 -0
- package/deployment-config/cli.ts +228 -0
- package/deployment-config/generate.ts +689 -0
- package/deployment-config/jsonc.ts +18 -0
- package/deployment-config/profile-schema.generated.ts +391 -0
- package/deployment-config/profile.schema.json +358 -0
- package/deployment-config/profile.ts +124 -0
- package/migrations/0001_better_auth.sql +15 -0
- package/migrations/0002_drop_account_issuer.sql +8 -0
- package/package.json +61 -5
- package/release-version.ts +20 -0
- package/src/account-admission.ts +179 -0
- package/src/account-deletion.ts +274 -0
- package/src/admin-entrypoint.ts +194 -0
- package/src/admin-identities.ts +26 -0
- package/src/audit.ts +103 -0
- package/src/auth-package.access.ts +24 -0
- package/src/auth-package.ts +36 -0
- package/src/avatar-state-cleanup.ts +107 -0
- package/src/billing-computer.ts +267 -0
- package/src/billing-readiness.ts +21 -0
- package/src/billing.ts +461 -0
- package/src/bot-capabilities.ts +480 -0
- package/src/bot-recovery.ts +1 -0
- package/src/bot-state-channel.ts +702 -0
- package/src/bot-state.ts +3995 -0
- package/src/bot-template-cleanup.ts +98 -0
- package/src/bot-title-cleanup.ts +104 -0
- package/src/brand-icon.png +0 -0
- package/src/brand-logo.ts +2 -0
- package/src/brand.ts +36 -0
- package/src/client-compatibility.ts +51 -0
- package/src/compaction-announcement-cleanup.ts +113 -0
- package/src/computer-egress.ts +110 -0
- package/src/computer-host.ts +92 -0
- package/src/computer-screenshot-cleanup.ts +78 -0
- package/src/contracts.ts +832 -0
- package/src/debug.ts +272 -0
- package/src/default-packages-marker-cleanup.ts +39 -0
- package/src/deployment-policy-admin-host.ts +121 -0
- package/src/deployment-policy.ts +426 -0
- package/src/directory-profile-cleanup.ts +101 -0
- package/src/durable-rpc.ts +753 -0
- package/src/durable-session.ts +21 -0
- package/src/entry-boundary.ts +103 -0
- package/src/frock-ai.ts +338 -0
- package/src/gateway.ts +1392 -0
- package/src/group-chat.ts +736 -0
- package/src/hidden-bot-notifications-cleanup.ts +32 -0
- package/src/index.ts +2798 -0
- package/src/insights.ts +15 -0
- package/src/machine-messages-cleanup.ts +121 -0
- package/src/machine-socket.ts +164 -0
- package/src/memory-records.ts +181 -0
- package/src/memory.ts +145 -0
- package/src/model-rates.ts +252 -0
- package/src/native-auth.ts +1006 -0
- package/src/native-sessions.ts +121 -0
- package/src/notification-state-cleanup.ts +18 -0
- package/src/ollama-web-search-cleanup.ts +69 -0
- package/src/package-page-shapes-cleanup.ts +190 -0
- package/src/plugin-egress.ts +31 -0
- package/src/plugin-page-route.ts +72 -0
- package/src/plugin-panels-cleanup.ts +101 -0
- package/src/prepared-input-cleanup.ts +105 -0
- package/src/production-secrets.ts +484 -0
- package/src/project-cleanup.ts +123 -0
- package/src/project-events-cleanup.ts +152 -0
- package/src/publication-state-cleanup.ts +92 -0
- package/src/push.ts +498 -0
- package/src/request-body.ts +165 -0
- package/src/routine-state-cleanup.ts +57 -0
- package/src/search.ts +88 -0
- package/src/sidebar-label-cleanup.ts +165 -0
- package/src/skill-index-cleanup.ts +53 -0
- package/src/supersede-cleanup.ts +162 -0
- package/src/test-chat-cleanup.ts +93 -0
- package/src/uploads.ts +486 -0
- package/src/user-application.ts +1068 -0
- package/src/user-configuration.ts +4491 -0
- package/src/voice-assistant.ts +4999 -0
- package/src/voice-dictation.ts +743 -0
- package/src/working-context-cleanup.ts +96 -0
- package/src/workspace.ts +201 -0
- package/wrangler.jsonc +385 -0
- 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>;
|