@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
package/src/debug.ts ADDED
@@ -0,0 +1,272 @@
1
+ import {
2
+ decodeBotDebugQueryV1,
3
+ isBotDebugQueryRefusalV1,
4
+ type BotDebugQueryV1,
5
+ } from "@frockbot/app/shell/debug-protocol";
6
+ import {
7
+ decodeSetUserFeaturesCommandV1,
8
+ type SetUserFeaturesCommandV1,
9
+ type UserFeaturesV1,
10
+ } from "@frockbot/app/admin/shared";
11
+ import { decodeBotIdV1 } from "@frockbot/core/configuration";
12
+ import { constantTimeEqualsV1 } from "@frockbot/core/crypto";
13
+
14
+ /**
15
+ * The operator surface: `/api/debug/*`, authorized by a shared token rather
16
+ * than by a session, so it can be read from a terminal while a Bot is wedged
17
+ * and nobody is signed in.
18
+ *
19
+ * Two routes write. A Turn send requires both the debug token and an
20
+ * owner/admin identity, then enters the ordinary client Turn path with its
21
+ * ownership check, admission/refusal handling and size limit. An account's
22
+ * features write is the operator's half of administration: the admin portal is
23
+ * the hosted deployment's surface for it (ADR 0028), and a deployment with no
24
+ * portal — a self-hosted one, a test stack — turns Plugin authoring on for an
25
+ * account from here, under the same deployment secret. Nothing here
26
+ * can reconcile, cancel, or otherwise move an existing run.
27
+ */
28
+ export interface DebugGatewaySurface {
29
+ /** Absent (or empty) disables the whole surface; the routes then 404. */
30
+ token?: string;
31
+ listUsers(): Promise<
32
+ Array<{ id: string; email: string; name: string; createdAt: string }>
33
+ >;
34
+ listBots(userId: string): Promise<unknown>;
35
+ snapshot(
36
+ userId: string,
37
+ botId: string,
38
+ query: BotDebugQueryV1,
39
+ ): Promise<unknown>;
40
+ /**
41
+ * The User's voice assistant ledger: calls, spoken turns with their
42
+ * transcripts, and the Bot delegations each made. Absent on a deployment
43
+ * with no voice objects; the route then 404s.
44
+ */
45
+ voice?(userId: string): Promise<unknown>;
46
+ isAdminUser(userId: string): Promise<boolean>;
47
+ setAccountFeatures(
48
+ userId: string,
49
+ command: SetUserFeaturesCommandV1,
50
+ ): Promise<UserFeaturesV1>;
51
+ }
52
+
53
+ export type DebugTurnSubmitter = (
54
+ userId: string,
55
+ botId: string,
56
+ text: string,
57
+ ) => Promise<Response>;
58
+
59
+ const DEBUG_PREFIX = "/api/debug";
60
+
61
+ function jsonError(status: number, message: string): Response {
62
+ return Response.json({ error: message }, { status });
63
+ }
64
+
65
+ function forbiddenUser(): Response {
66
+ return new Response(
67
+ "Debug Turn submission is only allowed for an administrator account.",
68
+ {
69
+ status: 403,
70
+ headers: { "content-type": "text/plain; charset=utf-8" },
71
+ },
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Length-independent, constant-time within a length: a token comparison that
77
+ * returns early leaks the token one character at a time.
78
+ */
79
+ function tokenMatches(presented: string, expected: string): boolean {
80
+ return constantTimeEqualsV1(presented, expected);
81
+ }
82
+
83
+ function presentedToken(request: Request): string | undefined {
84
+ const header = request.headers.get("authorization");
85
+ if (header?.toLowerCase().startsWith("bearer ")) {
86
+ return header.slice("bearer ".length).trim();
87
+ }
88
+ return request.headers.get("x-frockbot-debug-token")?.trim() ?? undefined;
89
+ }
90
+
91
+ /**
92
+ * The query the URL carries, decoded here rather than inside the Bot: the same
93
+ * decoder the Durable Object applies, run in the gateway so a query the
94
+ * operator got wrong is answered as a 400 and never becomes an uncaught
95
+ * failure in the Bot's isolate.
96
+ */
97
+ function debugQuery(url: URL): BotDebugQueryV1 {
98
+ const limit = url.searchParams.get("limit");
99
+ const before = url.searchParams.get("before");
100
+ const events = url.searchParams.get("events");
101
+ return decodeBotDebugQueryV1({
102
+ schemaVersion: 1,
103
+ // `Number("nonsense")` is `NaN`, which the decoder refuses with the same
104
+ // sentence a limit past the cap gets — one message for one bad field.
105
+ ...(limit === null ? {} : { limit: Number(limit) }),
106
+ ...(before === null ? {} : { before }),
107
+ ...(events === null ? {} : { events: events === "true" || events === "1" }),
108
+ });
109
+ }
110
+
111
+ function debugTurnText(input: unknown): string {
112
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
113
+ throw new Error("debug Turn body must be an object");
114
+ }
115
+ const body = input as Record<string, unknown>;
116
+ if (Object.keys(body).some((key) => key !== "text")) {
117
+ throw new Error("debug Turn body only accepts text");
118
+ }
119
+ if (typeof body.text !== "string") {
120
+ throw new Error("debug Turn body.text must be a string");
121
+ }
122
+ return body.text;
123
+ }
124
+
125
+ /**
126
+ * Mounted ahead of authentication in the gateway, so it answers with no
127
+ * session. Returns `undefined` for any path it does not own.
128
+ */
129
+ export function createDebugRoute(
130
+ surface: DebugGatewaySurface | undefined,
131
+ submitTurn?: DebugTurnSubmitter,
132
+ ): (request: Request, url: URL) => Promise<Response | undefined> {
133
+ return async (request, url) => {
134
+ if (
135
+ url.pathname !== DEBUG_PREFIX &&
136
+ !url.pathname.startsWith(`${DEBUG_PREFIX}/`)
137
+ ) {
138
+ return undefined;
139
+ }
140
+ // An unconfigured deployment does not admit that the surface exists.
141
+ if (!surface?.token) return jsonError(404, "not found");
142
+ const presented = presentedToken(request);
143
+ if (!presented || !tokenMatches(presented, surface.token)) {
144
+ return jsonError(401, "debug token required");
145
+ }
146
+
147
+ const path = url.pathname.slice(DEBUG_PREFIX.length);
148
+ try {
149
+ const sendMatch = path.match(/^\/users\/([^/]+)\/bots\/([^/]+)\/turns$/);
150
+ if (sendMatch) {
151
+ if (request.method !== "POST") {
152
+ return jsonError(405, "method not allowed");
153
+ }
154
+ const userId = decodeURIComponent(sendMatch[1]!);
155
+ const botId = decodeBotIdV1(decodeURIComponent(sendMatch[2]!));
156
+ // Resolve the durable sign-in identity before reading or forwarding
157
+ // the body. The debug token alone never gains authority to speak as an
158
+ // arbitrary User, and a User with no identity row is denied too.
159
+ if (!(await surface.isAdminUser(userId))) return forbiddenUser();
160
+ let text: string;
161
+ try {
162
+ text = debugTurnText(await request.json());
163
+ } catch (error) {
164
+ return jsonError(
165
+ 400,
166
+ error instanceof Error
167
+ ? error.message
168
+ : "debug Turn body is invalid",
169
+ );
170
+ }
171
+ // This is the only write on `/api/debug`: `submitTurn` forwards an
172
+ // ordinary client command to the loaded User application, so the
173
+ // response (including a 409 admission refusal or a 413 size refusal)
174
+ // is returned byte-for-byte with its original status.
175
+ if (!submitTurn)
176
+ return jsonError(503, "debug Turn send is unavailable");
177
+ return submitTurn(userId, botId, text);
178
+ }
179
+
180
+ const featuresMatch = path.match(/^\/users\/([^/]+)\/features$/);
181
+ if (featuresMatch) {
182
+ if (request.method !== "POST") {
183
+ return jsonError(405, "method not allowed");
184
+ }
185
+ const userId = decodeURIComponent(featuresMatch[1]!);
186
+ let command: SetUserFeaturesCommandV1;
187
+ try {
188
+ command = decodeSetUserFeaturesCommandV1(await request.json());
189
+ } catch (error) {
190
+ return jsonError(
191
+ 400,
192
+ error instanceof Error
193
+ ? error.message
194
+ : "account features command is invalid",
195
+ );
196
+ }
197
+ // The account is not provisioned or admitted by this write: the record
198
+ // is the account's own, and an account that never signs in keeps it.
199
+ return Response.json(await surface.setAccountFeatures(userId, command));
200
+ }
201
+
202
+ // Every other debug route is read-only.
203
+ if (request.method !== "GET") return jsonError(405, "method not allowed");
204
+ if (path === "" || path === "/") {
205
+ return Response.json({
206
+ schemaVersion: 1,
207
+ routes: [
208
+ "GET /api/debug/users",
209
+ "GET /api/debug/bots?userId=<id>",
210
+ "GET /api/debug/bots/<botId>?userId=<id>&limit=<n>&events=true&before=<cursor>",
211
+ "GET /api/debug/bots/<botId>/runs/<runId>?userId=<id>",
212
+ "GET /api/debug/voice?userId=<id>",
213
+ "POST /api/debug/users/<userId>/bots/<botId>/turns",
214
+ "POST /api/debug/users/<userId>/features",
215
+ ],
216
+ });
217
+ }
218
+ if (path === "/users") {
219
+ return Response.json({
220
+ schemaVersion: 1,
221
+ users: await surface.listUsers(),
222
+ });
223
+ }
224
+
225
+ const userId = url.searchParams.get("userId")?.trim();
226
+ if (path === "/bots") {
227
+ if (!userId) return jsonError(400, "userId is required");
228
+ return Response.json(await surface.listBots(userId));
229
+ }
230
+ if (path === "/voice") {
231
+ if (!surface.voice) return jsonError(404, "not found");
232
+ if (!userId) return jsonError(400, "userId is required");
233
+ return Response.json(await surface.voice(userId));
234
+ }
235
+ const runMatch = path.match(/^\/bots\/([^/]+)\/runs\/([^/]+)$/);
236
+ const botMatch = path.match(/^\/bots\/([^/]+)$/);
237
+ if (!runMatch && !botMatch) return jsonError(404, "not found");
238
+ if (!userId) return jsonError(400, "userId is required");
239
+ const botId = decodeBotIdV1(
240
+ decodeURIComponent((runMatch ?? botMatch)![1]!),
241
+ );
242
+ const query = runMatch
243
+ ? decodeBotDebugQueryV1({
244
+ schemaVersion: 1,
245
+ runId: decodeURIComponent(runMatch[2]!),
246
+ })
247
+ : debugQuery(url);
248
+ return Response.json(await surface.snapshot(userId, botId, query));
249
+ } catch (error) {
250
+ // A query the caller got wrong is their 400, not the deployment's 500 —
251
+ // and the message says the range so the next attempt can be right.
252
+ if (isBotDebugQueryRefusalV1(error)) {
253
+ return jsonError(
254
+ 400,
255
+ error instanceof Error ? error.message : "debug query is invalid",
256
+ );
257
+ }
258
+ // The operator is the audience: the message is the finding, and a stack
259
+ // that reached here is more useful than a generic 500.
260
+ return Response.json(
261
+ {
262
+ error:
263
+ error instanceof Error ? error.message : "debug request failed",
264
+ ...(error instanceof Error && error.stack
265
+ ? { stack: error.stack }
266
+ : {}),
267
+ },
268
+ { status: 500 },
269
+ );
270
+ }
271
+ };
272
+ }
@@ -0,0 +1,39 @@
1
+ // Disposable pre-user cleanup for retired default-Package bootstrap markers.
2
+ //
3
+ // Markers v1–v3 are `{ schemaVersion: 1 | 2 | 3 }` with no ledger. The decoder
4
+ // no longer accepts them. This pass deletes those records before any request
5
+ // or alarm can read settings, so the next read is the no-marker seed. A v4
6
+ // ledger is left alone. The receipt makes a second load free.
7
+
8
+ export const DEFAULT_PACKAGES_MARKER_KEY = "user-default-packages-bootstrap:v1";
9
+ export const DEFAULT_PACKAGES_MARKER_CLEANUP_RECEIPT_KEY =
10
+ "maintenance:default-packages-marker:2026-09-20";
11
+
12
+ export function isRetiredDefaultPackagesMarker(value: unknown): boolean {
13
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
14
+ return false;
15
+ }
16
+ const keys = Object.keys(value);
17
+ if (keys.length !== 1 || keys[0] !== "schemaVersion") return false;
18
+ const version = (value as { schemaVersion?: unknown }).schemaVersion;
19
+ return version === 1 || version === 2 || version === 3;
20
+ }
21
+
22
+ export async function cleanDefaultPackagesMarkerV1(
23
+ storage: DurableObjectStorage,
24
+ now: Date = new Date(),
25
+ ): Promise<void> {
26
+ if (await storage.get(DEFAULT_PACKAGES_MARKER_CLEANUP_RECEIPT_KEY)) return;
27
+ await storage.transaction(async (tx) => {
28
+ if (await tx.get(DEFAULT_PACKAGES_MARKER_CLEANUP_RECEIPT_KEY)) return;
29
+ const marker = await tx.get<unknown>(DEFAULT_PACKAGES_MARKER_KEY);
30
+ const removed = isRetiredDefaultPackagesMarker(marker);
31
+ if (removed) {
32
+ await tx.delete(DEFAULT_PACKAGES_MARKER_KEY);
33
+ }
34
+ await tx.put(DEFAULT_PACKAGES_MARKER_CLEANUP_RECEIPT_KEY, {
35
+ at: now.toISOString(),
36
+ removed,
37
+ });
38
+ });
39
+ }
@@ -0,0 +1,121 @@
1
+ import type { AdminOperationsHostV1 } from "@frockbot/app/admin/operations";
2
+ import {
3
+ AccountAccessConflictError,
4
+ decodeAccountAccessV1,
5
+ decodeAccountAccessViewV1,
6
+ decodeAdminWriteResultV1,
7
+ decodeDeploymentPolicyV1,
8
+ decodeEmailInvitationV1,
9
+ DeploymentPolicyConflictError,
10
+ } from "@frockbot/app/admin/shared";
11
+ import {
12
+ decodeHostedModelRatesV1,
13
+ decodeHostedModelRatesViewV1,
14
+ ModelRatesConflictError,
15
+ } from "@frockbot/app/billing/rates";
16
+ import { rpcJsonSnapshotV1 } from "./durable-rpc.js";
17
+
18
+ interface DeploymentPolicyAdminRpc {
19
+ readPolicy(input: unknown): Promise<unknown>;
20
+ setAdmissionMode(input: unknown): Promise<unknown>;
21
+ readAccountAccess(input: unknown): Promise<unknown>;
22
+ setAccountAccess(input: unknown): Promise<unknown>;
23
+ inviteEmail(input: unknown): Promise<unknown>;
24
+ readModelRatesView(input: unknown): Promise<unknown>;
25
+ saveModelRates(input: unknown): Promise<unknown>;
26
+ }
27
+
28
+ function appliedWrite(
29
+ answer: unknown,
30
+ conflict: (currentRevision: number) => Error,
31
+ ): unknown {
32
+ const write = rpcJsonSnapshotV1(answer) as Record<string, unknown> | null;
33
+ let decoded;
34
+ try {
35
+ decoded = decodeAdminWriteResultV1(
36
+ write,
37
+ (value) => value,
38
+ "access authority write result",
39
+ );
40
+ } catch {
41
+ throw new Error("access authority answered an unknown write result");
42
+ }
43
+ if (decoded.status === "conflict") throw conflict(decoded.currentRevision);
44
+ return decoded.value;
45
+ }
46
+
47
+ export function createDeploymentPolicyAdminHost(
48
+ authority: () => DeploymentPolicyAdminRpc,
49
+ ): Pick<
50
+ AdminOperationsHostV1,
51
+ | "readDeploymentPolicy"
52
+ | "setAdmissionMode"
53
+ | "readAccountAccess"
54
+ | "setAccountAccess"
55
+ | "inviteEmail"
56
+ | "readModelRates"
57
+ | "saveModelRates"
58
+ > {
59
+ return {
60
+ readDeploymentPolicy: async () =>
61
+ decodeDeploymentPolicyV1(
62
+ rpcJsonSnapshotV1(await authority().readPolicy({ schemaVersion: 1 })),
63
+ ),
64
+ setAdmissionMode: async (command, updatedBy) =>
65
+ decodeDeploymentPolicyV1(
66
+ appliedWrite(
67
+ await authority().setAdmissionMode({
68
+ schemaVersion: 1,
69
+ command,
70
+ updatedBy,
71
+ }),
72
+ (revision) => new DeploymentPolicyConflictError(revision),
73
+ ),
74
+ ),
75
+ readAccountAccess: async (userId) =>
76
+ decodeAccountAccessViewV1(
77
+ rpcJsonSnapshotV1(
78
+ await authority().readAccountAccess({ schemaVersion: 1, userId }),
79
+ ),
80
+ ),
81
+ setAccountAccess: async (userId, command, updatedBy) =>
82
+ decodeAccountAccessV1(
83
+ appliedWrite(
84
+ await authority().setAccountAccess({
85
+ schemaVersion: 1,
86
+ userId,
87
+ command,
88
+ updatedBy,
89
+ }),
90
+ (revision) => new AccountAccessConflictError(revision),
91
+ ),
92
+ ),
93
+ inviteEmail: async (command, invitedBy) =>
94
+ decodeEmailInvitationV1(
95
+ rpcJsonSnapshotV1(
96
+ await authority().inviteEmail({
97
+ schemaVersion: 1,
98
+ command,
99
+ invitedBy,
100
+ }),
101
+ ),
102
+ ),
103
+ readModelRates: async () =>
104
+ decodeHostedModelRatesViewV1(
105
+ rpcJsonSnapshotV1(
106
+ await authority().readModelRatesView({ schemaVersion: 1 }),
107
+ ),
108
+ ),
109
+ saveModelRates: async (command, createdBy) =>
110
+ decodeHostedModelRatesV1(
111
+ appliedWrite(
112
+ await authority().saveModelRates({
113
+ schemaVersion: 1,
114
+ command,
115
+ createdBy,
116
+ }),
117
+ (revision) => new ModelRatesConflictError(revision),
118
+ ),
119
+ ),
120
+ };
121
+ }