@frockbot/cloudflare 0.0.0 → 0.7.291

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/build-artifact.ts +130 -0
  2. package/build-flutter-web.ts +508 -0
  3. package/deployment-config/README.md +414 -0
  4. package/deployment-config/cli.ts +228 -0
  5. package/deployment-config/generate.ts +689 -0
  6. package/deployment-config/jsonc.ts +18 -0
  7. package/deployment-config/profile-schema.generated.ts +391 -0
  8. package/deployment-config/profile.schema.json +358 -0
  9. package/deployment-config/profile.ts +124 -0
  10. package/migrations/0001_better_auth.sql +15 -0
  11. package/migrations/0002_drop_account_issuer.sql +8 -0
  12. package/package.json +61 -5
  13. package/release-version.ts +20 -0
  14. package/src/account-admission.ts +179 -0
  15. package/src/account-deletion.ts +274 -0
  16. package/src/admin-entrypoint.ts +194 -0
  17. package/src/admin-identities.ts +26 -0
  18. package/src/audit.ts +103 -0
  19. package/src/auth-package.access.ts +24 -0
  20. package/src/auth-package.ts +36 -0
  21. package/src/avatar-state-cleanup.ts +107 -0
  22. package/src/billing-computer.ts +267 -0
  23. package/src/billing-readiness.ts +21 -0
  24. package/src/billing.ts +461 -0
  25. package/src/bot-capabilities.ts +480 -0
  26. package/src/bot-recovery.ts +1 -0
  27. package/src/bot-state-channel.ts +702 -0
  28. package/src/bot-state.ts +3995 -0
  29. package/src/bot-template-cleanup.ts +98 -0
  30. package/src/bot-title-cleanup.ts +104 -0
  31. package/src/brand-icon.png +0 -0
  32. package/src/brand-logo.ts +2 -0
  33. package/src/brand.ts +36 -0
  34. package/src/client-compatibility.ts +51 -0
  35. package/src/compaction-announcement-cleanup.ts +113 -0
  36. package/src/computer-egress.ts +110 -0
  37. package/src/computer-host.ts +92 -0
  38. package/src/computer-screenshot-cleanup.ts +78 -0
  39. package/src/contracts.ts +832 -0
  40. package/src/debug.ts +272 -0
  41. package/src/default-packages-marker-cleanup.ts +39 -0
  42. package/src/deployment-policy-admin-host.ts +121 -0
  43. package/src/deployment-policy.ts +426 -0
  44. package/src/directory-profile-cleanup.ts +101 -0
  45. package/src/durable-rpc.ts +753 -0
  46. package/src/durable-session.ts +21 -0
  47. package/src/entry-boundary.ts +103 -0
  48. package/src/frock-ai.ts +338 -0
  49. package/src/gateway.ts +1392 -0
  50. package/src/group-chat.ts +736 -0
  51. package/src/hidden-bot-notifications-cleanup.ts +32 -0
  52. package/src/index.ts +2798 -0
  53. package/src/insights.ts +15 -0
  54. package/src/machine-messages-cleanup.ts +121 -0
  55. package/src/machine-socket.ts +164 -0
  56. package/src/memory-records.ts +181 -0
  57. package/src/memory.ts +145 -0
  58. package/src/model-rates.ts +252 -0
  59. package/src/native-auth.ts +1006 -0
  60. package/src/native-sessions.ts +121 -0
  61. package/src/notification-state-cleanup.ts +18 -0
  62. package/src/ollama-web-search-cleanup.ts +69 -0
  63. package/src/package-page-shapes-cleanup.ts +190 -0
  64. package/src/plugin-egress.ts +31 -0
  65. package/src/plugin-page-route.ts +72 -0
  66. package/src/plugin-panels-cleanup.ts +101 -0
  67. package/src/prepared-input-cleanup.ts +105 -0
  68. package/src/production-secrets.ts +484 -0
  69. package/src/project-cleanup.ts +123 -0
  70. package/src/project-events-cleanup.ts +152 -0
  71. package/src/publication-state-cleanup.ts +92 -0
  72. package/src/push.ts +498 -0
  73. package/src/request-body.ts +165 -0
  74. package/src/routine-state-cleanup.ts +57 -0
  75. package/src/search.ts +88 -0
  76. package/src/sidebar-label-cleanup.ts +165 -0
  77. package/src/skill-index-cleanup.ts +53 -0
  78. package/src/supersede-cleanup.ts +162 -0
  79. package/src/test-chat-cleanup.ts +93 -0
  80. package/src/uploads.ts +486 -0
  81. package/src/user-application.ts +1068 -0
  82. package/src/user-configuration.ts +4491 -0
  83. package/src/voice-assistant.ts +4999 -0
  84. package/src/voice-dictation.ts +743 -0
  85. package/src/working-context-cleanup.ts +96 -0
  86. package/src/workspace.ts +201 -0
  87. package/wrangler.jsonc +385 -0
  88. package/README.md +0 -3
@@ -0,0 +1,98 @@
1
+ // Disposable pre-user cleanup for the removed Bot templates.
2
+ //
3
+ // Bot templates were removed. Their share ledger, import records and command
4
+ // receipts lived in the User Durable Object under `bot-template:`, each shared
5
+ // recipe was a content-addressed blob at `templates/<hash>.json` in the
6
+ // artifact bucket, and every account was seeded an installation row for the
7
+ // `bot-template` Package. This deletes the blob each share record names, then
8
+ // every `bot-template:` key, then that row. A blob is only ever written beside
9
+ // its share record, so the records are the whole index of what to delete. The
10
+ // receipt makes a second load free.
11
+
12
+ const RECEIPT = "maintenance:bot-template-removal:2026-09-25";
13
+ const PREFIX = "bot-template:";
14
+ const SHARE_PREFIX = `${PREFIX}share:`;
15
+ const USER_CONFIGURATION_KEY = "user-configuration";
16
+ const PACKAGE_ID = "bot-template";
17
+ const PAGE = 50;
18
+
19
+ interface CleanupStorage {
20
+ get(key: string): Promise<unknown>;
21
+ put(key: string, value: unknown): Promise<void>;
22
+ delete(key: string): Promise<boolean>;
23
+ list(options: {
24
+ prefix: string;
25
+ limit: number;
26
+ }): Promise<Map<string, unknown>>;
27
+ }
28
+
29
+ interface CleanupBucket {
30
+ delete(key: string): Promise<void>;
31
+ }
32
+
33
+ type Stored = Record<string, unknown>;
34
+
35
+ function record(value: unknown): Stored | undefined {
36
+ if (!value || typeof value !== "object" || Array.isArray(value))
37
+ return undefined;
38
+ return value as Stored;
39
+ }
40
+
41
+ /** The settings without the retired Package row, or `undefined` to keep them. */
42
+ export function withoutBotTemplatePackageV1(
43
+ stored: unknown,
44
+ ): Stored | undefined {
45
+ const settings = record(stored);
46
+ if (!settings || !Array.isArray(settings.packages)) return undefined;
47
+ const packages = settings.packages.filter(
48
+ (entry) => record(entry)?.packageId !== PACKAGE_ID,
49
+ );
50
+ return packages.length === settings.packages.length
51
+ ? undefined
52
+ : { ...settings, packages };
53
+ }
54
+
55
+ export async function cleanRetiredBotTemplatesV1(
56
+ storage: CleanupStorage,
57
+ bucket?: CleanupBucket,
58
+ now: Date = new Date(),
59
+ ): Promise<void> {
60
+ if (await storage.get(RECEIPT)) return;
61
+ let blobs = 0;
62
+ if (bucket) {
63
+ // Blobs go before their records: a wake cut short between the two finds
64
+ // the records still there and deletes the same keys again.
65
+ const hashes = new Set<string>();
66
+ for (const share of (
67
+ await storage.list({ prefix: SHARE_PREFIX, limit: 1_000 })
68
+ ).values()) {
69
+ const hash = record(share)?.hash;
70
+ if (typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash)) {
71
+ hashes.add(hash);
72
+ }
73
+ }
74
+ for (const hash of hashes) {
75
+ await bucket.delete(`templates/${hash}.json`);
76
+ blobs += 1;
77
+ }
78
+ }
79
+ let keys = 0;
80
+ for (;;) {
81
+ const listed = await storage.list({ prefix: PREFIX, limit: PAGE });
82
+ for (const key of listed.keys()) {
83
+ await storage.delete(key);
84
+ keys += 1;
85
+ }
86
+ if (listed.size < PAGE) break;
87
+ }
88
+ const settings = withoutBotTemplatePackageV1(
89
+ await storage.get(USER_CONFIGURATION_KEY),
90
+ );
91
+ if (settings) await storage.put(USER_CONFIGURATION_KEY, settings);
92
+ await storage.put(RECEIPT, {
93
+ at: now.toISOString(),
94
+ keys,
95
+ blobs,
96
+ packageRow: settings !== undefined,
97
+ });
98
+ }
@@ -0,0 +1,104 @@
1
+ // Disposable cleanup for the retired Bot title.
2
+ //
3
+ // A Bot's profile no longer has a `title`, and its exact decoder refuses one.
4
+ // The Bot's settings record loses it, with the revision moved so an open
5
+ // settings surface fences on the record it now reads. Every run keeps the
6
+ // settings it was admitted under twice — the configuration snapshot and the
7
+ // prepared inputs — and both are decoded whenever the run is read, so each
8
+ // loses the title too. The run walk pages rather than loading the archive
9
+ // into one value, and the receipt makes a second load free.
10
+
11
+ import { BOT_CONFIGURATION_KEY } from "@frockbot/app/settings/bot";
12
+ import { RUN_PREFIX } from "@frockbot/core/durable";
13
+
14
+ const RECEIPT = "maintenance:bot-title-removal:2026-09-25";
15
+ const PAGE = 50;
16
+
17
+ interface CleanupStorage {
18
+ get(key: string): Promise<unknown>;
19
+ put(key: string, value: unknown): Promise<void>;
20
+ list(options: {
21
+ prefix?: string;
22
+ limit?: number;
23
+ start?: string;
24
+ }): Promise<Map<string, unknown>>;
25
+ }
26
+
27
+ type Plain = Record<string, unknown>;
28
+
29
+ function record(value: unknown): Plain | undefined {
30
+ if (!value || typeof value !== "object" || Array.isArray(value))
31
+ return undefined;
32
+ return value as Plain;
33
+ }
34
+
35
+ /** `value` with `title` gone from the profile at `path`, or undefined. */
36
+ function withoutTitleAt(value: unknown, path: readonly string[]): unknown {
37
+ const outer = record(value);
38
+ if (!outer) return undefined;
39
+ if (path.length === 0) {
40
+ if (!Object.hasOwn(outer, "title")) return undefined;
41
+ const { title: _retired, ...rest } = outer;
42
+ return rest;
43
+ }
44
+ const [head, ...tail] = path as [string, ...string[]];
45
+ const inner = withoutTitleAt(outer[head], tail);
46
+ return inner === undefined ? undefined : { ...outer, [head]: inner };
47
+ }
48
+
49
+ const RUN_SETTINGS_PATHS = [
50
+ ["configurationSnapshot", "profile"],
51
+ ["preparedInputs", "bot", "settings", "profile"],
52
+ ] as const;
53
+
54
+ /** The run without either copy of the title; undefined when it had none. */
55
+ function runWithoutTitle(value: unknown): unknown {
56
+ let current = value;
57
+ let changed = false;
58
+ for (const path of RUN_SETTINGS_PATHS) {
59
+ const next = withoutTitleAt(current, path);
60
+ if (next === undefined) continue;
61
+ current = next;
62
+ changed = true;
63
+ }
64
+ return changed ? current : undefined;
65
+ }
66
+
67
+ export async function cleanBotTitleV1(storage: CleanupStorage): Promise<void> {
68
+ if (await storage.get(RECEIPT)) return;
69
+ let settings = false;
70
+ const stored = record(await storage.get(BOT_CONFIGURATION_KEY));
71
+ const cleaned = record(withoutTitleAt(stored, ["profile"]));
72
+ if (stored && cleaned && typeof stored.revision === "number") {
73
+ await storage.put(BOT_CONFIGURATION_KEY, {
74
+ ...cleaned,
75
+ revision: stored.revision + 1,
76
+ });
77
+ settings = true;
78
+ }
79
+ let runs = 0;
80
+ let start: string | undefined;
81
+ for (;;) {
82
+ const listed = await storage.list({
83
+ prefix: RUN_PREFIX,
84
+ limit: PAGE,
85
+ ...(start ? { start } : {}),
86
+ });
87
+ let last = "";
88
+ for (const [key, value] of listed) {
89
+ last = key;
90
+ const run = runWithoutTitle(value);
91
+ if (run === undefined) continue;
92
+ runs += 1;
93
+ await storage.put(key, run);
94
+ }
95
+ if (listed.size < PAGE) break;
96
+ start = `${last}\0`;
97
+ }
98
+ await storage.put(RECEIPT, {
99
+ schemaVersion: 1,
100
+ settings,
101
+ runs,
102
+ completedAt: new Date().toISOString(),
103
+ });
104
+ }
Binary file
@@ -0,0 +1,2 @@
1
+ export const FROCKBOT_PAGE_LOGO_V1 =
2
+ "data:image/webp;base64,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";
package/src/brand.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * FrockBot's brand: what the hosted deployment and every suite show a person.
3
+ *
4
+ * The Worker reaches it through `#brand`, which `apps/cloudflare/package.json`
5
+ * maps here. A profile that names its own brand module gets a generated
6
+ * wrangler config aliasing `#brand` to that file instead, the way `access`
7
+ * profiles alias `#auth-package` ([ADR 0038](../../../docs/adr/0038-white-label-deployments.md)).
8
+ * Only the Worker imports this; app code is handed the brand as data.
9
+ */
10
+ import type { BrandV1 } from "@frockbot/core/contracts";
11
+ import {
12
+ INK_DOCUMENT_V1,
13
+ PAPER_DOCUMENT_V1,
14
+ STUDIO_DOCUMENT_V1,
15
+ } from "@frockbot/core/theme";
16
+ import { FROCKBOT_PAGE_LOGO_V1 } from "./brand-logo.js";
17
+
18
+ export const BRAND_V1: BrandV1 = {
19
+ schemaVersion: 1,
20
+ productName: "FrockBot",
21
+ homepage: "https://frockbot.com",
22
+ builtInModelName: "Frock AI",
23
+ emailSenderName: "FrockBot",
24
+ // A copy of the canonical `assets/marketing/app-icon/frockbot-icon-64.png`,
25
+ // kept inside the package so the published `@frockbot/cloudflare` builds
26
+ // its default brand too; `brand.test.ts` holds the two to the same bytes.
27
+ iconPng: "./brand-icon.png",
28
+ pageLogo: FROCKBOT_PAGE_LOGO_V1,
29
+ nativeScheme: "frockbot",
30
+ looks: {
31
+ ink: INK_DOCUMENT_V1,
32
+ paper: PAPER_DOCUMENT_V1,
33
+ studio: STUDIO_DOCUMENT_V1,
34
+ },
35
+ whatsNew: true,
36
+ };
@@ -0,0 +1,51 @@
1
+ import {
2
+ CLIENT_COMPATIBILITY,
3
+ SUPPORTED_PROTOCOL_MAX,
4
+ SUPPORTED_PROTOCOL_MIN,
5
+ isProtocolValue,
6
+ } from "@frockbot/core/protocol-schemas";
7
+ import { BRAND_V1 } from "#brand";
8
+
9
+ export const CLIENT_HELLO_HEADER = "x-frockbot-client";
10
+ export const UPDATE_APP_MESSAGE = `Update the app to continue using ${BRAND_V1.productName}.`;
11
+
12
+ /** Version selection is compatibility, never authentication or a capability grant. */
13
+ export function clientCompatibilityResponse(
14
+ request: Request,
15
+ url: URL,
16
+ ): Response | undefined {
17
+ const header = request.headers.get(CLIENT_HELLO_HEADER);
18
+ const nativeRoute =
19
+ url.pathname.startsWith("/api/auth/native/") ||
20
+ url.pathname.startsWith("/api/native/");
21
+ if (header !== null || nativeRoute) {
22
+ let hello: unknown;
23
+ try {
24
+ hello = header && header.length <= 4096 ? JSON.parse(header) : undefined;
25
+ } catch {
26
+ hello = undefined;
27
+ }
28
+ if (
29
+ !isProtocolValue("ClientHello", hello) ||
30
+ hello.protocolVersion < SUPPORTED_PROTOCOL_MIN ||
31
+ hello.protocolVersion > SUPPORTED_PROTOCOL_MAX
32
+ ) {
33
+ return new Response(UPDATE_APP_MESSAGE, {
34
+ status: 426,
35
+ headers: {
36
+ "content-type": "text/plain; charset=utf-8",
37
+ "cache-control": "no-store",
38
+ },
39
+ });
40
+ }
41
+ }
42
+ if (
43
+ url.pathname === "/api/client-compatibility" &&
44
+ request.method === "GET"
45
+ ) {
46
+ return Response.json(CLIENT_COMPATIBILITY, {
47
+ headers: { "cache-control": "no-store" },
48
+ });
49
+ }
50
+ return undefined;
51
+ }
@@ -0,0 +1,113 @@
1
+ // Disposable cleanup for the compaction lines the conversation used to
2
+ // publish. The wire no longer has a `conversation/compacted` announcement, so
3
+ // a stored row or replayed update carrying one fails a current client's
4
+ // schema and breaks its state channel.
5
+ //
6
+ // The replay log cannot lose an entry in the middle without a gap, so it is
7
+ // trimmed through the newest compaction update instead: a client behind that
8
+ // point gets the ordinary gap reset and reads the cleaned snapshot.
9
+
10
+ import {
11
+ CONVERSATION_VISIBLE_INDEX_KEY,
12
+ PUBLICATION_HEAD_KEY,
13
+ PUBLICATION_PENDING_PREFIX,
14
+ conversationRowKeyV1,
15
+ conversationUpdateKeyV1,
16
+ readPublicationHeadV1,
17
+ readVisibleIndexV1,
18
+ } from "@frockbot/core/durable";
19
+
20
+ const RECEIPT = "maintenance:compaction-announcements:2026-09-25";
21
+ const COMPACTION_ENTITY_PREFIX = "ann:compaction-";
22
+ // Durable Object storage deletes at most 128 keys per call, and this runs in
23
+ // the Bot's constructor, where a throw keeps the Bot from starting.
24
+ const DELETE_BATCH = 128;
25
+
26
+ interface CleanupStorage {
27
+ get<T = unknown>(key: string): Promise<T | undefined>;
28
+ put(key: string, value: unknown): Promise<void>;
29
+ delete(key: string | string[]): Promise<boolean | number | void>;
30
+ list<T = unknown>(options: { prefix?: string }): Promise<Map<string, T>>;
31
+ }
32
+
33
+ async function deleteAll(
34
+ storage: CleanupStorage,
35
+ keys: string[],
36
+ ): Promise<void> {
37
+ for (let start = 0; start < keys.length; start += DELETE_BATCH) {
38
+ await storage.delete(keys.slice(start, start + DELETE_BATCH));
39
+ }
40
+ }
41
+
42
+ function isCompactionEntity(value: unknown): boolean {
43
+ return (
44
+ typeof value === "object" &&
45
+ value !== null &&
46
+ "entityId" in value &&
47
+ typeof value.entityId === "string" &&
48
+ value.entityId.startsWith(COMPACTION_ENTITY_PREFIX)
49
+ );
50
+ }
51
+
52
+ export async function cleanCompactionAnnouncementsV1(
53
+ storage: CleanupStorage,
54
+ ): Promise<void> {
55
+ if (await storage.get(RECEIPT)) return;
56
+ let removed = 0;
57
+
58
+ const index = await readVisibleIndexV1(storage);
59
+ const compactions = index.announcementEntityIds.filter((id) =>
60
+ id.startsWith(COMPACTION_ENTITY_PREFIX),
61
+ );
62
+ if (compactions.length > 0) {
63
+ await storage.put(CONVERSATION_VISIBLE_INDEX_KEY, {
64
+ ...index,
65
+ announcementEntityIds: index.announcementEntityIds.filter(
66
+ (id) => !id.startsWith(COMPACTION_ENTITY_PREFIX),
67
+ ),
68
+ });
69
+ await deleteAll(storage, compactions.map(conversationRowKeyV1));
70
+ removed += compactions.length;
71
+ }
72
+
73
+ const head = await readPublicationHeadV1(storage);
74
+ let through = 0;
75
+ for (
76
+ let cursor = head.firstRetainedCursor;
77
+ cursor <= head.lastCursor;
78
+ cursor += 1
79
+ ) {
80
+ const update = await storage.get(conversationUpdateKeyV1(cursor));
81
+ if (isCompactionEntity(update)) through = cursor;
82
+ }
83
+ if (through > 0) {
84
+ const expired: string[] = [];
85
+ for (let cursor = head.firstRetainedCursor; cursor <= through; cursor++) {
86
+ expired.push(conversationUpdateKeyV1(cursor));
87
+ }
88
+ await deleteAll(storage, expired);
89
+ // An undrained broadcast at or before the trim point is either a
90
+ // compaction nobody should see or an update the snapshot now carries.
91
+ const pending = await storage.list({ prefix: PUBLICATION_PENDING_PREFIX });
92
+ const stale = [...pending].filter(
93
+ ([, value]) =>
94
+ typeof value === "object" &&
95
+ value !== null &&
96
+ "cursor" in value &&
97
+ typeof value.cursor === "number" &&
98
+ value.cursor <= through,
99
+ );
100
+ await deleteAll(
101
+ storage,
102
+ stale.map(([key]) => key),
103
+ );
104
+ await storage.put(PUBLICATION_HEAD_KEY, {
105
+ ...head,
106
+ firstRetainedCursor: through + 1,
107
+ broadcastThrough: Math.max(head.broadcastThrough, through),
108
+ });
109
+ removed += expired.length;
110
+ }
111
+
112
+ await storage.put(RECEIPT, { schemaVersion: 1, removed });
113
+ }
@@ -0,0 +1,110 @@
1
+ // The door the Computer's connected-account proxy posts to. It is on the open
2
+ // internet, so the token is verified before any object is addressed: only a
3
+ // token this deployment signed names one.
4
+ import {
5
+ COMPUTER_EGRESS_PATH_V1,
6
+ decodeComputerEgressRequestV1,
7
+ decodeComputerEgressResponseV1,
8
+ verifyComputerEgressTokenV1,
9
+ type ComputerEgressRequestV1,
10
+ } from "@frockbot/computer/egress";
11
+
12
+ export { COMPUTER_EGRESS_PATH_V1 };
13
+
14
+ /** A forwarded request: a 1 MB body in base64 plus its headers and URL. */
15
+ const MAX_BODY_BYTES = 1_500_000;
16
+
17
+ export interface ComputerEgressAnswerInputV1 {
18
+ schemaVersion: 1;
19
+ object: string;
20
+ nonce: string;
21
+ request: ComputerEgressRequestV1;
22
+ }
23
+
24
+ export function decodeComputerEgressAnswerInputV1(
25
+ value: unknown,
26
+ ): ComputerEgressAnswerInputV1 {
27
+ const record =
28
+ value && typeof value === "object"
29
+ ? (value as Record<string, unknown>)
30
+ : {};
31
+ const request = decodeComputerEgressRequestV1(record.request);
32
+ if (
33
+ record.schemaVersion !== 1 ||
34
+ typeof record.object !== "string" ||
35
+ typeof record.nonce !== "string" ||
36
+ !request
37
+ ) {
38
+ throw new Error("computer egress input is invalid");
39
+ }
40
+ return {
41
+ schemaVersion: 1,
42
+ object: record.object,
43
+ nonce: record.nonce,
44
+ request,
45
+ };
46
+ }
47
+
48
+ function refuse(status: number, error: string): Response {
49
+ return Response.json(
50
+ { error },
51
+ { status, headers: { "cache-control": "no-store" } },
52
+ );
53
+ }
54
+
55
+ export async function routeComputerEgressV1(
56
+ request: Request,
57
+ dependencies: {
58
+ /** The signing secret; absent, the door is closed. */
59
+ secret?: string;
60
+ answer(input: ComputerEgressAnswerInputV1): Promise<unknown>;
61
+ now?: number;
62
+ },
63
+ ): Promise<Response> {
64
+ if (request.method !== "POST") return refuse(405, "method not allowed");
65
+ if (!dependencies.secret) return refuse(503, "computer egress is off");
66
+ const authorization = request.headers.get("authorization") ?? "";
67
+ const token = authorization.startsWith("Bearer ")
68
+ ? await verifyComputerEgressTokenV1(
69
+ dependencies.secret,
70
+ authorization.slice(7).trim(),
71
+ dependencies.now,
72
+ )
73
+ : undefined;
74
+ if (!token) return refuse(401, "token is invalid or expired");
75
+ const length = Number(request.headers.get("content-length") ?? "0");
76
+ if (length > MAX_BODY_BYTES) return refuse(413, "request is too large");
77
+ let text: string;
78
+ try {
79
+ text = await request.text();
80
+ } catch {
81
+ return refuse(400, "request could not be read");
82
+ }
83
+ if (new TextEncoder().encode(text).byteLength > MAX_BODY_BYTES) {
84
+ return refuse(413, "request is too large");
85
+ }
86
+ let forwarded: ComputerEgressRequestV1 | undefined;
87
+ try {
88
+ forwarded = decodeComputerEgressRequestV1(JSON.parse(text));
89
+ } catch {
90
+ forwarded = undefined;
91
+ }
92
+ if (!forwarded) return refuse(400, "request is invalid");
93
+ let answer: unknown;
94
+ try {
95
+ answer = await dependencies.answer({
96
+ schemaVersion: 1,
97
+ object: token.o,
98
+ nonce: token.n,
99
+ request: forwarded,
100
+ });
101
+ } catch (error) {
102
+ console.error("computer egress failed", error);
103
+ return refuse(502, "the request could not be answered");
104
+ }
105
+ const response = decodeComputerEgressResponseV1(answer);
106
+ if (!response) return refuse(502, "the request could not be answered");
107
+ return Response.json(response, {
108
+ headers: { "cache-control": "no-store" },
109
+ });
110
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The one place this deployment chooses its Computer host.
3
+ *
4
+ * `ComputerHostV1` is the interface everything above the Computer speaks
5
+ * (`computer/core/host.ts`); Fly is one implementation of it and the in-memory
6
+ * host in `computer/fake` is another. Which one this Worker runs is a
7
+ * deployment decision, not an application one, so it is made here — beside the
8
+ * bindings the choice depends on — and handed to the app as a factory.
9
+ *
10
+ * `scripts/check-computer-host-imports.ts` rule 2 admits `@frockbot/computer/fly`
11
+ * into this file, `computer/fly/**`, `apps/computer-host/**` and the two test
12
+ * rigs that prove the implementation. Substituting a k8s host is this file and
13
+ * nothing else.
14
+ */
15
+ import type { ShellComputerHostOptionsV1 } from "@frockbot/app/shell/backend-runtime";
16
+ import type {
17
+ ComputerHostCapabilitiesV1,
18
+ ComputerHostV1,
19
+ } from "@frockbot/computer/core/host";
20
+ import { FlyHostTransportV1 } from "@frockbot/computer/fly/host-client";
21
+ import {
22
+ FLY_HOST_CAPABILITIES_V1,
23
+ FlyComputerHostV1,
24
+ } from "@frockbot/computer/fly/provider";
25
+
26
+ /**
27
+ * What this deployment's Computer host is, for the surfaces that must know
28
+ * before a Bot is running — the app's `frame-src` above all, which is built
29
+ * from `viewerFrameOrigins` while no Computer is open.
30
+ */
31
+ export const COMPUTER_HOST_CAPABILITIES_V1: ComputerHostCapabilitiesV1 =
32
+ FLY_HOST_CAPABILITIES_V1;
33
+
34
+ /**
35
+ * The `COMPUTER_HOST` service binding and the secret presented on it.
36
+ *
37
+ * Both or neither: a binding with no token reaches a host that refuses every
38
+ * call, which would surface as a 401 on each Turn rather than as a Computer
39
+ * that is not configured.
40
+ */
41
+ export interface ComputerHostBindingV1 {
42
+ fetcher: { fetch(request: Request): Promise<Response> };
43
+ hostToken: string;
44
+ }
45
+
46
+ /** What this deployment's Worker needs before it has a Computer at all. */
47
+ export interface ComputerHostEnvV1 {
48
+ COMPUTER_HOST?: { fetch(request: Request): Promise<Response> };
49
+ COMPUTER_HOST_TOKEN?: string;
50
+ }
51
+
52
+ /**
53
+ * The binding this deployment reaches its Computer host on, when it has one.
54
+ *
55
+ * This is the whole of "is a Computer configured", and it is derived here so
56
+ * that the app above can answer the question by the presence of a host and
57
+ * never by the name of a credential. The vendor's own token is the host app's:
58
+ * it reaches `apps/computer-host` and never this Worker, which could not use
59
+ * one if it had it.
60
+ */
61
+ export function computerHostBindingV1(
62
+ env: ComputerHostEnvV1,
63
+ ): ComputerHostBindingV1 | undefined {
64
+ const fetcher = env.COMPUTER_HOST;
65
+ const hostToken = env.COMPUTER_HOST_TOKEN?.trim();
66
+ return fetcher && hostToken ? { fetcher, hostToken } : undefined;
67
+ }
68
+
69
+ /**
70
+ * The Computer host, over one binding.
71
+ *
72
+ * The Fly implementation holds no credential of its own — the Computer host
73
+ * app holds the only copy — so the whole of what a caller supplies here is the
74
+ * binding to send a call over and the seams the Turn owns.
75
+ */
76
+ export function createComputerHostV1(
77
+ binding: ComputerHostBindingV1,
78
+ options: ShellComputerHostOptionsV1 = {},
79
+ ): ComputerHostV1 {
80
+ return new FlyComputerHostV1(
81
+ undefined,
82
+ (identity, tenant) =>
83
+ new FlyHostTransportV1({
84
+ fetcher: binding.fetcher,
85
+ hostToken: binding.hostToken,
86
+ identity,
87
+ tenant,
88
+ }),
89
+ options.sync,
90
+ options.agentControlOwnerId,
91
+ );
92
+ }