@frockbot/cloudflare 0.0.0 → 0.7.292

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/build-artifact.ts +130 -0
  2. package/build-flutter-web.ts +508 -0
  3. package/deployment-config/README.md +414 -0
  4. package/deployment-config/cli.ts +228 -0
  5. package/deployment-config/generate.ts +689 -0
  6. package/deployment-config/jsonc.ts +18 -0
  7. package/deployment-config/profile-schema.generated.ts +391 -0
  8. package/deployment-config/profile.schema.json +358 -0
  9. package/deployment-config/profile.ts +124 -0
  10. package/migrations/0001_better_auth.sql +15 -0
  11. package/migrations/0002_drop_account_issuer.sql +8 -0
  12. package/package.json +61 -5
  13. package/release-version.ts +20 -0
  14. package/src/account-admission.ts +179 -0
  15. package/src/account-deletion.ts +274 -0
  16. package/src/admin-entrypoint.ts +194 -0
  17. package/src/admin-identities.ts +26 -0
  18. package/src/audit.ts +103 -0
  19. package/src/auth-package.access.ts +24 -0
  20. package/src/auth-package.ts +36 -0
  21. package/src/avatar-state-cleanup.ts +107 -0
  22. package/src/billing-computer.ts +267 -0
  23. package/src/billing-readiness.ts +21 -0
  24. package/src/billing.ts +461 -0
  25. package/src/bot-capabilities.ts +480 -0
  26. package/src/bot-recovery.ts +1 -0
  27. package/src/bot-state-channel.ts +702 -0
  28. package/src/bot-state.ts +3995 -0
  29. package/src/bot-template-cleanup.ts +98 -0
  30. package/src/bot-title-cleanup.ts +104 -0
  31. package/src/brand-icon.png +0 -0
  32. package/src/brand-logo.ts +2 -0
  33. package/src/brand.ts +36 -0
  34. package/src/client-compatibility.ts +51 -0
  35. package/src/compaction-announcement-cleanup.ts +113 -0
  36. package/src/computer-egress.ts +110 -0
  37. package/src/computer-host.ts +92 -0
  38. package/src/computer-screenshot-cleanup.ts +78 -0
  39. package/src/contracts.ts +832 -0
  40. package/src/debug.ts +272 -0
  41. package/src/default-packages-marker-cleanup.ts +39 -0
  42. package/src/deployment-policy-admin-host.ts +121 -0
  43. package/src/deployment-policy.ts +426 -0
  44. package/src/directory-profile-cleanup.ts +101 -0
  45. package/src/durable-rpc.ts +753 -0
  46. package/src/durable-session.ts +21 -0
  47. package/src/entry-boundary.ts +103 -0
  48. package/src/frock-ai.ts +338 -0
  49. package/src/gateway.ts +1392 -0
  50. package/src/group-chat.ts +736 -0
  51. package/src/hidden-bot-notifications-cleanup.ts +32 -0
  52. package/src/index.ts +2798 -0
  53. package/src/insights.ts +15 -0
  54. package/src/machine-messages-cleanup.ts +121 -0
  55. package/src/machine-socket.ts +164 -0
  56. package/src/memory-records.ts +181 -0
  57. package/src/memory.ts +145 -0
  58. package/src/model-rates.ts +252 -0
  59. package/src/native-auth.ts +1006 -0
  60. package/src/native-sessions.ts +121 -0
  61. package/src/notification-state-cleanup.ts +18 -0
  62. package/src/ollama-web-search-cleanup.ts +69 -0
  63. package/src/package-page-shapes-cleanup.ts +190 -0
  64. package/src/plugin-egress.ts +31 -0
  65. package/src/plugin-page-route.ts +72 -0
  66. package/src/plugin-panels-cleanup.ts +101 -0
  67. package/src/prepared-input-cleanup.ts +105 -0
  68. package/src/production-secrets.ts +484 -0
  69. package/src/project-cleanup.ts +123 -0
  70. package/src/project-events-cleanup.ts +152 -0
  71. package/src/publication-state-cleanup.ts +92 -0
  72. package/src/push.ts +498 -0
  73. package/src/request-body.ts +165 -0
  74. package/src/routine-state-cleanup.ts +57 -0
  75. package/src/search.ts +88 -0
  76. package/src/sidebar-label-cleanup.ts +165 -0
  77. package/src/skill-index-cleanup.ts +53 -0
  78. package/src/supersede-cleanup.ts +162 -0
  79. package/src/test-chat-cleanup.ts +93 -0
  80. package/src/uploads.ts +486 -0
  81. package/src/user-application.ts +1068 -0
  82. package/src/user-configuration.ts +4491 -0
  83. package/src/voice-assistant.ts +4999 -0
  84. package/src/voice-dictation.ts +743 -0
  85. package/src/working-context-cleanup.ts +96 -0
  86. package/src/workspace.ts +201 -0
  87. package/wrangler.jsonc +385 -0
  88. package/README.md +0 -3
@@ -0,0 +1,753 @@
1
+ import {
2
+ decodeSkillRefsV1,
3
+ decodeUploadRefsV1,
4
+ type SkillRefV1,
5
+ type UploadRefV1,
6
+ } from "@frockbot/core/contracts";
7
+ import { decodeBotIdV1, isRpcIdentifier } from "@frockbot/core/configuration";
8
+ import { decodeRunIdV1 } from "@frockbot/app/shell/backend-contracts";
9
+ import {
10
+ decodeStoredRunCauseV1,
11
+ type StoredRunCauseV1,
12
+ type StoredRunEmailOriginV1,
13
+ type StoredRunGroupOriginV1,
14
+ } from "@frockbot/core/durable";
15
+ import {
16
+ VOICE_CALL_TRANSCRIPT_TEXT_MAX_V1,
17
+ VOICE_CALL_TRANSCRIPT_TURNS_MAX_V1,
18
+ type VoiceCallTranscriptTurnV1,
19
+ } from "@frockbot/core/contracts";
20
+
21
+ export { rpcJsonSnapshotV1 } from "@frockbot/app/durable-rpc";
22
+
23
+ type RpcValueDecoder = (value: unknown, label: string) => unknown;
24
+ export type RpcJsonValue =
25
+ | null
26
+ | boolean
27
+ | number
28
+ | string
29
+ | RpcJsonValue[]
30
+ | { [key: string]: RpcJsonValue };
31
+
32
+ function record(value: unknown, label: string): Record<string, unknown> {
33
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
34
+ throw new Error(`${label} must be an object`);
35
+ }
36
+ return value as Record<string, unknown>;
37
+ }
38
+
39
+ export function decodeRpcEnvelopeV1(
40
+ input: unknown,
41
+ required: Readonly<Record<string, RpcValueDecoder>>,
42
+ optional: Readonly<Record<string, RpcValueDecoder>> = {},
43
+ ): Record<string, unknown> {
44
+ const value = record(input, "RPC request");
45
+ const allowed = new Set([
46
+ "schemaVersion",
47
+ ...Object.keys(required),
48
+ ...Object.keys(optional),
49
+ ]);
50
+ if (
51
+ value.schemaVersion !== 1 ||
52
+ !Object.keys(required).every((key) => Object.hasOwn(value, key)) ||
53
+ !Object.keys(value).every((key) => allowed.has(key))
54
+ ) {
55
+ throw new Error("RPC request is invalid");
56
+ }
57
+ const decoded: Record<string, unknown> = { schemaVersion: 1 };
58
+ for (const [key, decoder] of Object.entries(required)) {
59
+ decoded[key] = decoder(value[key], `RPC request.${key}`);
60
+ }
61
+ for (const [key, decoder] of Object.entries(optional)) {
62
+ if (value[key] !== undefined) {
63
+ decoded[key] = decoder(value[key], `RPC request.${key}`);
64
+ }
65
+ }
66
+ return decoded;
67
+ }
68
+
69
+ export function rpcString(maximum = 4_096): RpcValueDecoder {
70
+ return (value, label) => {
71
+ if (
72
+ typeof value !== "string" ||
73
+ value.length === 0 ||
74
+ value.length > maximum
75
+ ) {
76
+ throw new Error(`${label} must be a bounded non-empty string`);
77
+ }
78
+ return value;
79
+ };
80
+ }
81
+
82
+ export const rpcIdentifier: RpcValueDecoder = (value, label) => {
83
+ if (!isRpcIdentifier(value)) {
84
+ throw new Error(`${label} must be an identifier`);
85
+ }
86
+ return value;
87
+ };
88
+
89
+ /** An http(s) origin and nothing else: no path, query or credentials. */
90
+ export const rpcOrigin: RpcValueDecoder = (value, label) => {
91
+ if (typeof value === "string" && value.length <= 256) {
92
+ try {
93
+ const url = new URL(value);
94
+ if (
95
+ (url.protocol === "https:" || url.protocol === "http:") &&
96
+ url.origin === value
97
+ ) {
98
+ return value;
99
+ }
100
+ } catch {
101
+ // Falls through to the refusal below.
102
+ }
103
+ }
104
+ throw new Error(`${label} must be an http(s) origin`);
105
+ };
106
+
107
+ export const rpcBotId: RpcValueDecoder = (value, label) => {
108
+ try {
109
+ return decodeBotIdV1(value);
110
+ } catch {
111
+ throw new Error(`${label} must be a Bot ID`);
112
+ }
113
+ };
114
+
115
+ export function rpcInteger(bounds: {
116
+ minimum: number;
117
+ maximum: number;
118
+ }): RpcValueDecoder {
119
+ return (value, label) => {
120
+ if (
121
+ !Number.isSafeInteger(value) ||
122
+ (value as number) < bounds.minimum ||
123
+ (value as number) > bounds.maximum
124
+ ) {
125
+ throw new Error(`${label} must be a bounded integer`);
126
+ }
127
+ return value;
128
+ };
129
+ }
130
+
131
+ export function rpcEnum<const T extends readonly string[]>(
132
+ values: T,
133
+ ): RpcValueDecoder {
134
+ return (value, label) => {
135
+ if (typeof value !== "string" || !values.includes(value)) {
136
+ throw new Error(`${label} is invalid`);
137
+ }
138
+ return value;
139
+ };
140
+ }
141
+
142
+ export function rpcPattern(pattern: RegExp, maximum = 256): RpcValueDecoder {
143
+ return (value, label) => {
144
+ if (
145
+ typeof value !== "string" ||
146
+ value.length === 0 ||
147
+ value.length > maximum ||
148
+ !pattern.test(value)
149
+ ) {
150
+ throw new Error(`${label} is invalid`);
151
+ }
152
+ return value;
153
+ };
154
+ }
155
+
156
+ /** A Plugin id, or `null` to close the conversation panel. */
157
+ export const rpcPluginIdOrNull: RpcValueDecoder = (value, label) => {
158
+ if (value === null) return null;
159
+ if (
160
+ typeof value !== "string" ||
161
+ value.length === 0 ||
162
+ value.length > 64 ||
163
+ !/^[a-z][a-z0-9-]{0,63}$/.test(value)
164
+ ) {
165
+ throw new Error(`${label} must be a plugin id or null`);
166
+ }
167
+ return value;
168
+ };
169
+
170
+ export const rpcBoolean: RpcValueDecoder = (value, label) => {
171
+ if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
172
+ return value;
173
+ };
174
+
175
+ export function rpcArray(
176
+ item: RpcValueDecoder,
177
+ maximum: number,
178
+ ): RpcValueDecoder {
179
+ return (value, label) => {
180
+ if (!Array.isArray(value) || value.length > maximum) {
181
+ throw new Error(`${label} must be a bounded array`);
182
+ }
183
+ return value.map((entry, index) => item(entry, `${label}[${index}]`));
184
+ };
185
+ }
186
+
187
+ export function rpcText(maximum: number): RpcValueDecoder {
188
+ return (value, label) => {
189
+ if (typeof value !== "string" || value.length > maximum) {
190
+ throw new Error(`${label} must be a bounded string`);
191
+ }
192
+ return value;
193
+ };
194
+ }
195
+
196
+ export function rpcObject(
197
+ required: Readonly<Record<string, RpcValueDecoder>>,
198
+ optional: Readonly<Record<string, RpcValueDecoder>> = {},
199
+ ): RpcValueDecoder {
200
+ return (value, label) => {
201
+ const source = record(value, label);
202
+ const allowed = new Set([
203
+ ...Object.keys(required),
204
+ ...Object.keys(optional),
205
+ ]);
206
+ if (
207
+ !Object.keys(required).every((key) => Object.hasOwn(source, key)) ||
208
+ !Object.keys(source).every((key) => allowed.has(key))
209
+ ) {
210
+ throw new Error(`${label} is invalid`);
211
+ }
212
+ const decoded: Record<string, unknown> = {};
213
+ for (const [key, decoder] of Object.entries(required)) {
214
+ decoded[key] = decoder(source[key], `${label}.${key}`);
215
+ }
216
+ for (const [key, decoder] of Object.entries(optional)) {
217
+ if (source[key] !== undefined) {
218
+ decoded[key] = decoder(source[key], `${label}.${key}`);
219
+ }
220
+ }
221
+ return decoded;
222
+ };
223
+ }
224
+
225
+ function cloneJson(value: unknown, label: string, depth: number): RpcJsonValue {
226
+ if (depth > 8) throw new Error(`${label} is too deeply nested`);
227
+ if (
228
+ value === null ||
229
+ typeof value === "boolean" ||
230
+ (typeof value === "number" && Number.isFinite(value))
231
+ ) {
232
+ return value;
233
+ }
234
+ if (typeof value === "string") {
235
+ if (value.length > 8_192) throw new Error(`${label} is too long`);
236
+ return value;
237
+ }
238
+ if (Array.isArray(value)) {
239
+ if (value.length > 256) throw new Error(`${label} has too many values`);
240
+ return value.map((entry, index) =>
241
+ cloneJson(entry, `${label}[${index}]`, depth + 1),
242
+ );
243
+ }
244
+ const source = record(value, label);
245
+ const entries = Object.entries(source);
246
+ if (entries.length > 256) throw new Error(`${label} has too many fields`);
247
+ return Object.fromEntries(
248
+ entries.map(([key, entry]) => {
249
+ if (!key || key.length > 128)
250
+ throw new Error(`${label} has an invalid key`);
251
+ return [key, cloneJson(entry, `${label}.${key}`, depth + 1)];
252
+ }),
253
+ );
254
+ }
255
+
256
+ export const rpcJsonRecord: RpcValueDecoder = (value, label) =>
257
+ cloneJson(record(value, label), label, 0);
258
+
259
+ /**
260
+ * Carries an inbound value through the envelope decoder unchanged, for a
261
+ * caller that decodes it against a richer contract of its own — a Workspace
262
+ * root, a generation record — immediately afterwards. It is never a way to
263
+ * skip decoding: the value is still refused before it reaches durable state.
264
+ */
265
+ export const rpcDecodedValue: RpcValueDecoder = (value, label) => {
266
+ if (!value || typeof value !== "object") {
267
+ throw new Error(`${label} must be an object`);
268
+ }
269
+ return value;
270
+ };
271
+
272
+ export function rpcDecoded(
273
+ decoder: (input: unknown) => unknown,
274
+ ): RpcValueDecoder {
275
+ return (value) => decoder(value);
276
+ }
277
+
278
+ export interface DecodedStartConnectionRpcV1 {
279
+ schemaVersion: 1;
280
+ userId: string;
281
+ connection: {
282
+ connectionId: string;
283
+ packageId: string;
284
+ connectionTypeId: string;
285
+ displayName: string;
286
+ safeMetadata?: { [key: string]: RpcJsonValue };
287
+ };
288
+ }
289
+
290
+ export function decodeStartConnectionRpcV1(
291
+ input: unknown,
292
+ ): DecodedStartConnectionRpcV1 {
293
+ const request = decodeRpcEnvelopeV1(input, {
294
+ userId: rpcIdentifier,
295
+ connection: rpcObject(
296
+ {
297
+ connectionId: rpcIdentifier,
298
+ packageId: rpcIdentifier,
299
+ connectionTypeId: rpcIdentifier,
300
+ displayName: rpcString(256),
301
+ },
302
+ { safeMetadata: rpcJsonRecord },
303
+ ),
304
+ });
305
+ return {
306
+ schemaVersion: 1,
307
+ userId: request.userId as string,
308
+ connection: request.connection as DecodedStartConnectionRpcV1["connection"],
309
+ };
310
+ }
311
+
312
+ export interface DecodedBotRunRpcV1 {
313
+ schemaVersion: 1;
314
+ userId: string;
315
+ botId: string;
316
+ command: {
317
+ runId: string;
318
+ sessionId: string;
319
+ acceptedAt: string;
320
+ text: string;
321
+ skills?: SkillRefV1[];
322
+ /** Refs only: the Bot resolves each against its own uploads. */
323
+ attachments?: UploadRefV1[];
324
+ retryOf?: string;
325
+ };
326
+ }
327
+
328
+ /**
329
+ * A Turn command as it crosses an RPC door: its files are refs, and only the
330
+ * Bot Durable Object that holds the uploads turns them into attachments.
331
+ */
332
+ export type BotTurnCommandRequestV1<Command extends object> = Omit<
333
+ Command,
334
+ "attachments"
335
+ > & { attachments?: readonly UploadRefV1[] };
336
+
337
+ /**
338
+ * A Turn command's text. Empty is allowed here and nowhere else: a message may
339
+ * be files alone, which {@link requireTurnCommandContentV1} checks once the
340
+ * whole command is decoded.
341
+ */
342
+ export function rpcTurnText(maximum: number): RpcValueDecoder {
343
+ return (value, label) => {
344
+ if (typeof value !== "string" || value.length > maximum) {
345
+ throw new Error(`${label} must be a bounded string`);
346
+ }
347
+ return value;
348
+ };
349
+ }
350
+
351
+ /** A command carries words, files, or both — never neither. */
352
+ export function requireTurnCommandContentV1(command: {
353
+ text: string;
354
+ attachments?: readonly unknown[];
355
+ }): void {
356
+ if (command.text.trim().length === 0 && !command.attachments?.length) {
357
+ throw new Error("RPC request.command.text is required");
358
+ }
359
+ }
360
+
361
+ /**
362
+ * The optional members of a Turn command, shared by every door a Turn command
363
+ * crosses — the User application's `run` and the Bot Durable Object's — so a
364
+ * field one door accepts is never one another door rejects.
365
+ *
366
+ * Invoked Skills cross the RPC as refs and are decoded here, at the door,
367
+ * exactly like every other inbound value. Neither a Turn type nor a lane is
368
+ * carried here: only the composer reaches this door, and an absent lane is
369
+ * already the person's own.
370
+ */
371
+ export const rpcBotTurnCommandOptionalsV1: Readonly<
372
+ Record<string, RpcValueDecoder>
373
+ > = {
374
+ skills: (value, label) => decodeSkillRefsV1(value, label),
375
+ attachments: (value, label) => decodeUploadRefsV1(value, label),
376
+ retryOf: (value) => decodeRunIdV1(value),
377
+ };
378
+
379
+ export function decodeBotRunRpcV1(input: unknown): DecodedBotRunRpcV1 {
380
+ const request = decodeRpcEnvelopeV1(input, {
381
+ userId: rpcIdentifier,
382
+ botId: rpcBotId,
383
+ command: rpcObject(
384
+ {
385
+ runId: rpcString(128),
386
+ sessionId: rpcString(257),
387
+ acceptedAt: rpcString(64),
388
+ text: rpcTurnText(32_000),
389
+ },
390
+ rpcBotTurnCommandOptionalsV1,
391
+ ),
392
+ });
393
+ const command = request.command as DecodedBotRunRpcV1["command"];
394
+ requireTurnCommandContentV1(command);
395
+ command.runId = decodeRunIdV1(command.runId);
396
+ if (!Number.isFinite(Date.parse(command.acceptedAt))) {
397
+ throw new Error("RPC request.command.acceptedAt is invalid");
398
+ }
399
+ if (new TextEncoder().encode(command.text).byteLength > 32_000) {
400
+ throw new Error("RPC request.command.text is invalid");
401
+ }
402
+ return {
403
+ schemaVersion: 1,
404
+ userId: request.userId as string,
405
+ botId: request.botId as string,
406
+ command,
407
+ };
408
+ }
409
+
410
+ export interface DecodedBotAgentRunRpcV1 {
411
+ schemaVersion: 1;
412
+ userId: string;
413
+ botId: string;
414
+ command: {
415
+ runId: string;
416
+ sessionId: string;
417
+ acceptedAt: string;
418
+ text: string;
419
+ source: {
420
+ kind: "bot";
421
+ fromBotId: string;
422
+ fromBotName: string;
423
+ messageId: string;
424
+ cause?: StoredRunCauseV1;
425
+ };
426
+ };
427
+ }
428
+
429
+ /** What started the asking Turn, decoded as the run record decodes it. */
430
+ const rpcRunCause: RpcValueDecoder = (value, label) => {
431
+ try {
432
+ return decodeStoredRunCauseV1(value, label);
433
+ } catch {
434
+ throw new Error(`${label} is invalid`);
435
+ }
436
+ };
437
+
438
+ export interface DecodedBotVoiceRunRpcV1 {
439
+ schemaVersion: 1;
440
+ userId: string;
441
+ botId: string;
442
+ command: {
443
+ runId: string;
444
+ sessionId: string;
445
+ acceptedAt: string;
446
+ text: string;
447
+ source: {
448
+ kind: "voice";
449
+ callId: string;
450
+ voiceTurnId: string;
451
+ requestId: string;
452
+ };
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Internal-only voice admission. Its own door rather than a variant of the
458
+ * agent one: the return address a voice request carries is not a Bot id, and a
459
+ * door that accepted either could be handed a Bot's name where a call belongs.
460
+ * The HTTP Turn decoder cannot name this shape at all.
461
+ */
462
+ export function decodeBotVoiceRunRpcV1(
463
+ input: unknown,
464
+ ): DecodedBotVoiceRunRpcV1 {
465
+ const source = rpcObject({
466
+ kind: rpcPattern(/^voice$/, 5),
467
+ callId: rpcString(128),
468
+ voiceTurnId: rpcString(256),
469
+ requestId: rpcString(128),
470
+ });
471
+ const request = decodeRpcEnvelopeV1(input, {
472
+ userId: rpcIdentifier,
473
+ botId: rpcBotId,
474
+ command: rpcObject({
475
+ runId: rpcString(128),
476
+ sessionId: rpcString(257),
477
+ acceptedAt: rpcString(64),
478
+ text: rpcString(32_000),
479
+ source,
480
+ }),
481
+ });
482
+ const command = request.command as DecodedBotVoiceRunRpcV1["command"];
483
+ command.runId = decodeRunIdV1(command.runId);
484
+ // The request the answer goes back to is the Turn that answers it. A door
485
+ // that let the two differ would let one admission address another's caller.
486
+ if (command.source.requestId !== command.runId) {
487
+ throw new Error("voice RPC request.command.source.requestId is invalid");
488
+ }
489
+ if (!Number.isFinite(Date.parse(command.acceptedAt))) {
490
+ throw new Error("voice RPC request.command.acceptedAt is invalid");
491
+ }
492
+ if (new TextEncoder().encode(command.text).byteLength > 32_000) {
493
+ throw new Error("voice RPC request.command.text is invalid");
494
+ }
495
+ return {
496
+ schemaVersion: 1,
497
+ userId: request.userId as string,
498
+ botId: request.botId as string,
499
+ command,
500
+ };
501
+ }
502
+
503
+ export interface DecodedVoiceChatResultRpcV1 {
504
+ schemaVersion: 1;
505
+ userId: string;
506
+ botId: string;
507
+ command: {
508
+ runId: string;
509
+ body: string;
510
+ ordinal: number;
511
+ };
512
+ }
513
+
514
+ /**
515
+ * A settled voice request written into the Bot's thread after hang-up.
516
+ * Internal-only: the voice object is the only caller.
517
+ */
518
+ export function decodeVoiceChatResultRpcV1(
519
+ input: unknown,
520
+ ): DecodedVoiceChatResultRpcV1 {
521
+ const request = decodeRpcEnvelopeV1(input, {
522
+ userId: rpcIdentifier,
523
+ botId: rpcBotId,
524
+ command: rpcObject({
525
+ runId: rpcString(128),
526
+ body: rpcString(4_000),
527
+ ordinal: rpcInteger({ minimum: 0, maximum: 64 }),
528
+ }),
529
+ });
530
+ const command = request.command as DecodedVoiceChatResultRpcV1["command"];
531
+ command.runId = decodeRunIdV1(command.runId);
532
+ return {
533
+ schemaVersion: 1,
534
+ userId: request.userId as string,
535
+ botId: request.botId as string,
536
+ command,
537
+ };
538
+ }
539
+
540
+ export interface DecodedVoiceCallTranscriptRpcV1 {
541
+ schemaVersion: 1;
542
+ userId: string;
543
+ botId: string;
544
+ command: {
545
+ callId: string;
546
+ startedAt: string;
547
+ endedAt: string;
548
+ turns: VoiceCallTranscriptTurnV1[];
549
+ };
550
+ }
551
+
552
+ function rpcInstant(): RpcValueDecoder {
553
+ return (value, label) => {
554
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
555
+ throw new Error(`${label} is invalid`);
556
+ }
557
+ return value;
558
+ };
559
+ }
560
+
561
+ /**
562
+ * Spoken turns of one ended call, written onto the Bot's thread.
563
+ * Internal-only: the voice object is the only caller.
564
+ */
565
+ export function decodeVoiceCallTranscriptRpcV1(
566
+ input: unknown,
567
+ ): DecodedVoiceCallTranscriptRpcV1 {
568
+ const request = decodeRpcEnvelopeV1(input, {
569
+ userId: rpcIdentifier,
570
+ botId: rpcBotId,
571
+ command: rpcObject({
572
+ callId: rpcString(128),
573
+ startedAt: rpcInstant(),
574
+ endedAt: rpcInstant(),
575
+ turns: rpcArray(
576
+ rpcObject(
577
+ { transcript: rpcText(VOICE_CALL_TRANSCRIPT_TEXT_MAX_V1) },
578
+ { answer: rpcText(VOICE_CALL_TRANSCRIPT_TEXT_MAX_V1) },
579
+ ),
580
+ VOICE_CALL_TRANSCRIPT_TURNS_MAX_V1,
581
+ ),
582
+ }),
583
+ });
584
+ const command = request.command as DecodedVoiceCallTranscriptRpcV1["command"];
585
+ if (command.turns.length === 0) {
586
+ throw new Error("RPC request.command.turns is invalid");
587
+ }
588
+ return {
589
+ schemaVersion: 1,
590
+ userId: request.userId as string,
591
+ botId: request.botId as string,
592
+ command,
593
+ };
594
+ }
595
+
596
+ /** Internal-only agent admission; the HTTP Turn decoder cannot name it. */
597
+ export function decodeBotAgentRunRpcV1(
598
+ input: unknown,
599
+ ): DecodedBotAgentRunRpcV1 {
600
+ const source = rpcObject(
601
+ {
602
+ kind: rpcPattern(/^bot$/, 3),
603
+ fromBotId: rpcBotId,
604
+ fromBotName: rpcString(100),
605
+ messageId: rpcString(256),
606
+ },
607
+ { cause: rpcRunCause },
608
+ );
609
+ const request = decodeRpcEnvelopeV1(input, {
610
+ userId: rpcIdentifier,
611
+ botId: rpcBotId,
612
+ command: rpcObject({
613
+ runId: rpcString(128),
614
+ sessionId: rpcString(257),
615
+ acceptedAt: rpcString(64),
616
+ text: rpcString(32_000),
617
+ source,
618
+ }),
619
+ });
620
+ const command = request.command as DecodedBotAgentRunRpcV1["command"];
621
+ command.runId = decodeRunIdV1(command.runId);
622
+ if (!Number.isFinite(Date.parse(command.acceptedAt))) {
623
+ throw new Error("agent RPC request.command.acceptedAt is invalid");
624
+ }
625
+ if (new TextEncoder().encode(command.text).byteLength > 32_000) {
626
+ throw new Error("agent RPC request.command.text is invalid");
627
+ }
628
+ return {
629
+ schemaVersion: 1,
630
+ userId: request.userId as string,
631
+ botId: request.botId as string,
632
+ command,
633
+ };
634
+ }
635
+
636
+ export interface DecodedBotEmailTurnRpcV1 {
637
+ schemaVersion: 1;
638
+ userId: string;
639
+ botId: string;
640
+ command: {
641
+ runId: string;
642
+ sessionId: string;
643
+ acceptedAt: string;
644
+ text: string;
645
+ /** Refs only: the Bot resolves each against its own uploads. */
646
+ attachments?: UploadRefV1[];
647
+ origin: StoredRunEmailOriginV1;
648
+ };
649
+ }
650
+
651
+ /**
652
+ * Internal-only email admission: the person, writing from one of their
653
+ * confirmed mailboxes, into this Bot's own conversation. Its own door so that
654
+ * the one caller able to name an `email` origin is the Worker's `email()`
655
+ * handler, after the User object accepted the sender; the HTTP Turn decoder
656
+ * cannot name it.
657
+ */
658
+ export function decodeBotEmailTurnRpcV1(
659
+ input: unknown,
660
+ ): DecodedBotEmailTurnRpcV1 {
661
+ const request = decodeRpcEnvelopeV1(input, {
662
+ userId: rpcIdentifier,
663
+ botId: rpcBotId,
664
+ command: rpcObject(
665
+ {
666
+ runId: rpcPattern(/^em-[0-9a-f]{64}$/, 67),
667
+ sessionId: rpcString(257),
668
+ acceptedAt: rpcString(64),
669
+ text: rpcTurnText(32_000),
670
+ origin: rpcObject({
671
+ kind: rpcPattern(/^email$/, 5),
672
+ messageId: rpcPattern(/^[\x21-\x3b\x3d\x3f-\x7e]{1,250}$/, 250),
673
+ }),
674
+ },
675
+ {
676
+ attachments: (value, label) => decodeUploadRefsV1(value, label),
677
+ },
678
+ ),
679
+ });
680
+ const userId = request.userId as string;
681
+ const botId = request.botId as string;
682
+ const command = request.command as DecodedBotEmailTurnRpcV1["command"];
683
+ requireTurnCommandContentV1(command);
684
+ if (command.sessionId !== `${userId}:${botId}`) {
685
+ throw new Error("email RPC request.command.sessionId is invalid");
686
+ }
687
+ if (!Number.isFinite(Date.parse(command.acceptedAt))) {
688
+ throw new Error("email RPC request.command.acceptedAt is invalid");
689
+ }
690
+ if (new TextEncoder().encode(command.text).byteLength > 32_000) {
691
+ throw new Error("email RPC request.command.text is invalid");
692
+ }
693
+ return { schemaVersion: 1, userId, botId, command };
694
+ }
695
+
696
+ export interface DecodedBotGroupTurnRpcV1 {
697
+ schemaVersion: 1;
698
+ userId: string;
699
+ botId: string;
700
+ command: {
701
+ runId: string;
702
+ sessionId: string;
703
+ acceptedAt: string;
704
+ text: string;
705
+ origin: StoredRunGroupOriginV1;
706
+ };
707
+ }
708
+
709
+ /**
710
+ * Internal-only group admission: a Group Chat's object asking one of its
711
+ * members for a Turn. Its own door, like voice's: the return address is a
712
+ * group, and the Session is that group's, never the member's one-to-one chat.
713
+ */
714
+ export function decodeBotGroupTurnRpcV1(
715
+ input: unknown,
716
+ ): DecodedBotGroupTurnRpcV1 {
717
+ const origin = rpcObject({
718
+ kind: rpcPattern(/^group$/, 5),
719
+ groupId: rpcPattern(/^g-[0-9a-f]{20}$/, 22),
720
+ groupName: rpcText(1_000),
721
+ members: rpcArray(rpcObject({ botId: rpcBotId, name: rpcText(100) }), 8),
722
+ throughSeq: rpcInteger({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
723
+ reason: rpcEnum(["mention", "continue", "retry", "jev"] as const),
724
+ });
725
+ const request = decodeRpcEnvelopeV1(input, {
726
+ userId: rpcIdentifier,
727
+ botId: rpcBotId,
728
+ command: rpcObject({
729
+ runId: rpcString(128),
730
+ sessionId: rpcString(257),
731
+ acceptedAt: rpcString(64),
732
+ text: rpcString(32_000),
733
+ origin,
734
+ }),
735
+ });
736
+ const command = request.command as DecodedBotGroupTurnRpcV1["command"];
737
+ command.runId = decodeRunIdV1(command.runId);
738
+ if (command.sessionId !== `group:${command.origin.groupId}`) {
739
+ throw new Error("group RPC request.command.sessionId is invalid");
740
+ }
741
+ if (!Number.isFinite(Date.parse(command.acceptedAt))) {
742
+ throw new Error("group RPC request.command.acceptedAt is invalid");
743
+ }
744
+ if (command.origin.members.length === 0) {
745
+ throw new Error("group RPC request.command.origin.members is invalid");
746
+ }
747
+ return {
748
+ schemaVersion: 1,
749
+ userId: request.userId as string,
750
+ botId: request.botId as string,
751
+ command,
752
+ };
753
+ }