Skip to content
370 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

What g1t can't do yet, and an open letter to Cloudflare1/**
2 * Cloudflare's API, behind the calls deployments need: open an upload of
3 * an app's files, put the app in the dispatch namespace, take it down, and
4 * count what each app used.
5 *
6 * The token is the service's own, scoped to Workers scripts and analytics on
7 * g1t's account. It never leaves this Worker: a sandbox only ever gets an
8 * upload session's key, which can upload one manifest's files and nothing
9 * else.
10 */
11
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r912import { APP_CLASS, APP_WORKER, REGISTRY_HOST, applicationName, applicationRequest, pushLoginRequest, rolloutRequest } from "./containers";
What g1t can't do yet, and an open letter to Cloudflare13import { redirectScript } from "./redirect";
14
15const API = "https://api.cloudflare.com/client/v4";
16
17export type Manifest = Record<string, { hash: string; size: number }>;
18
19/** A module of a Worker, as the sandbox sends it. */
20export type Module = { name: string; contentBase64: string; contentType: string };
21
22/** What the sandbox built: the Worker's code and settings. */
23export type BuiltWorker = {
24 mainModule?: string;
25 modules?: Module[];
26 compatibilityDate?: string;
27 compatibilityFlags?: string[];
28 vars?: Record<string, unknown>;
29 assetsBinding?: string | null;
30 htmlHandling?: string | null;
31 notFoundHandling?: string | null;
32 _headers?: string;
33 _redirects?: string;
34};
35
36/** Serves the site's files, for an app that brings no code of its own. */
37const ASSETS_ONLY = `export default { fetch(request, env) { return env.ASSETS.fetch(request); } };\n`;
38
39/** What an app answers between being taken down and being gone. */
40const PAUSED = `export default {
41 fetch() {
42 return new Response("<!doctype html><meta charset=utf-8><meta name=robots content=noindex><title>Paused</title><body style='font:16px system-ui;background:#121214;color:#ececf1;display:grid;place-items:center;min-height:100vh;margin:0;padding:0 16px'><main style='max-width:32rem'><h1>This app is paused</h1><p style='color:#9a9aa6'>The workspace it belongs to reached its usage limit on g1t. It comes back by itself once the workspace is under it again.</p></main>", { status: 402, headers: { "content-type": "text/html; charset=utf-8", "x-robots-tag": "noindex", "cache-control": "no-store" } });
43 },
44};`;
45
46const TAKEN_DOWN = `export default {
47 fetch() {
48 return new Response("<!doctype html><meta charset=utf-8><meta name=robots content=noindex><title>Not up</title><body style='font:16px system-ui;background:#121214;color:#ececf1;display:grid;place-items:center;min-height:100vh;margin:0'><main><h1>This app is not up</h1><p style='color:#9a9aa6'>It was taken down. Deploying it again brings it back.</p></main>", { status: 404, headers: { "content-type": "text/html; charset=utf-8", "x-robots-tag": "noindex", "cache-control": "no-store" } });
49 },
50};
51`;
52
53const HTML_HANDLING =["auto-trailing-slash", "force-trailing-slash", "drop-trailing-slash", "none"];
54const NOT_FOUND_HANDLING = ["single-page-application", "404-page", "none"];
55
56export class Cloudflare {
57 constructor(
58 private readonly token: string,
59 private readonly account: string,
60 readonly namespace: string,
61 ) {}
62
63 private async call<T>(method: string, path: string, body?: BodyInit, contentType?: string): Promise<T> {
64 const headers: Record<string, string> = { authorization: `Bearer ${this.token}` };
65 if (contentType) headers["content-type"] = contentType;
66 const response = await fetch(`${API}${path}`, { method, headers, body });
67 const answer = (await response.json().catch(() => null)) as {
68 success?: boolean;
69 result?: T;
70 errors?: { code: number; message: string }[];
71 } | null;
72 if (!response.ok || !answer?.success) {
73 const why = answer?.errors?.map((error) => `${error.message} (${error.code})`).join("; ");
74 throw new Error(`Cloudflare answered ${response.status}: ${why || "no reason given"}`);
75 }
76 return answer.result as T;
77 }
78
79 private scriptPath(script: string): string {
80 return `/accounts/${this.account}/workers/dispatch/namespaces/${this.namespace}/scripts/${encodeURIComponent(script)}`;
81 }
82
83 /** Where a sandbox sends the files an upload session asks for. */
84 get uploadUrl(): string {
85 return `${API}/accounts/${this.account}/workers/assets/upload?base64=true`;
86 }
87
88 /**
89 * Opens an upload of exactly these files. Cloudflare answers with a key
90 * that can upload only them, and the files it does not already have, in
91 * buckets; with no buckets, the key itself completes the upload.
92 */
93 async openUpload(script: string, manifest: Manifest): Promise<{ jwt: string; buckets: string[][] }> {
94 const result = await this.call<{ jwt: string; buckets?: string[][] }>(
95 "POST",
96 `${this.scriptPath(script)}/assets-upload-session`,
97 JSON.stringify({ manifest }),
98 "application/json",
99 );
100 return { jwt: result.jwt, buckets: result.buckets ?? [] };
101 }
102
103 /** Puts an app in the namespace, replacing what was there. */
104 async putScript(
105 script: string,
106 worker: BuiltWorker,
107 completionJwt: string | null,
108 tags: string[],
109 /** The repository's entries for running apps, over the project's own `vars`. */
110 runtime: { secrets: Record<string, string>; variables: Record<string, string> } = { secrets: {}, variables: {} },
111 ): Promise<void> {
112 const form = new FormData();
113 const modules = worker.modules?.length ? worker.modules : null;
114 const assetsBinding = worker.assetsBinding || "ASSETS";
115 const vars: Record<string, unknown> = { ...(worker.vars ?? {}), ...runtime.variables };
116 for (const name of Object.keys(runtime.secrets)) delete vars[name];
117 const bindings: object[] = Object.entries(vars).map(([name, value]) =>
118 typeof value === "string"
119 ? { type: "plain_text", name, text: value }
120 : { type: "json", name, json: value },
121 );
122 for (const [name, text] of Object.entries(runtime.secrets)) bindings.push({ type: "secret_text", name, text });
123 if (completionJwt) bindings.push({ type: "assets", name: assetsBinding });
124 const assetsConfig: Record<string, string> = {};
125 if (worker.htmlHandling && HTML_HANDLING.includes(worker.htmlHandling)) {
126 assetsConfig.html_handling = worker.htmlHandling;
127 }
128 if (worker.notFoundHandling && NOT_FOUND_HANDLING.includes(worker.notFoundHandling)) {
129 assetsConfig.not_found_handling = worker.notFoundHandling;
130 }
131 if (worker._headers) assetsConfig._headers = worker._headers;
132 if (worker._redirects) assetsConfig._redirects = worker._redirects;
133 const mainModule = modules ? worker.mainModule ?? modules[0].name : "index.js";
134 form.append(
135 "metadata",
136 JSON.stringify({
137 main_module: mainModule,
138 compatibility_date: worker.compatibilityDate ?? "2026-09-26",
139 compatibility_flags: worker.compatibilityFlags ?? [],
140 bindings,
141 tags,
142 ...(completionJwt ? { assets: { jwt: completionJwt, config: assetsConfig } } : {}),
143 }),
144 );
145 if (modules) {
146 for (const module of modules) {
147 const bytes = Uint8Array.from(atob(module.contentBase64), (c) => c.charCodeAt(0));
148 form.append(module.name, new File([bytes], module.name, { type: module.contentType }));
149 }
150 } else {
151 if (!completionJwt) throw new Error("The build produced neither code nor files to serve.");
152 form.append(
153 "index.js",
154 new File([ASSETS_ONLY], "index.js", { type: "application/javascript+module" }),
155 );
156 }
157 await this.call("PUT", this.scriptPath(script), form);
158 }
159
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9160 // ---- Container apps (containers.ts) ---------------------------------
161
162 /**
163 * A login to Cloudflare's registry for this account: push only (never
164 * pull, so a build cannot read another app's image), for minutes.
165 */
166 async registryLogin(permissions: ("push" | "pull")[] = ["push"]): Promise<{ username: string; password: string }> {
167 const body = permissions.includes("pull") ? { permissions, expiration_minutes: 5 } : pushLoginRequest();
168 const result = await this.call<{ username: string; password?: string }>(
169 "POST",
170 `/accounts/${this.account}/containers/registries/${REGISTRY_HOST}/credentials`,
171 JSON.stringify(body),
172 "application/json",
173 );
174 if (!result.password) throw new Error("Cloudflare's registry gave no password.");
175 return { username: result.username, password: result.password };
176 }
177
178 /**
179 * Whether `repository` holds the image `digest`: asked of the registry
180 * itself, with a pull login of g1t's own, through the token exchange
181 * registries use (`WWW-Authenticate: Bearer`).
182 */
183 async hasImage(repository: string, digest: string): Promise<boolean> {
184 const login = await this.registryLogin(["pull"]);
185 const basic = `Basic ${btoa(`${login.username}:${login.password}`)}`;
186 const url = `https://${REGISTRY_HOST}/v2/${repository}/manifests/${digest}`;
187 const accept = "application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json";
188 let response = await fetch(url, { method: "HEAD", headers: { authorization: basic, accept } });
189 const challenge = response.headers.get("www-authenticate") ?? "";
190 if (response.status === 401 && /^bearer /i.test(challenge)) {
191 const params = Object.fromEntries([...challenge.matchAll(/(\w+)="([^"]*)"/g)].map((m) => [m[1], m[2]]));
192 const token = new URL(params.realm);
193 if (params.service) token.searchParams.set("service", params.service);
194 token.searchParams.set("scope", params.scope ?? `repository:${repository}:pull`);
195 const answer = (await (await fetch(token, { headers: { authorization: basic } })).json().catch(() => null)) as {
196 token?: string;
197 access_token?: string;
198 } | null;
199 const bearer = answer?.token ?? answer?.access_token;
200 if (!bearer) return false;
201 response = await fetch(url, { method: "HEAD", headers: { authorization: `Bearer ${bearer}`, accept } });
202 }
203 return response.ok;
204 }
205
206 /** Puts a container app's Worker (`APP_WORKER`, g1t's own) in the namespace, with `metadata`. */
207 async putContainerWorker(script: string, metadata: Record<string, unknown>): Promise<void> {
208 const form = new FormData();
209 form.append("metadata", JSON.stringify(metadata));
210 form.append("index.js", new File([APP_WORKER], "index.js", { type: "application/javascript+module" }));
211 await this.call("PUT", this.scriptPath(script), form);
212 }
213
214 /** The Durable Object namespace of a container app's Worker: what its Container application is attached to. */
215 async appNamespace(script: string): Promise<string> {
216 for (let page = 1; page <= 20; page++) {
217 const namespaces = await this.call<{ id: string; class: string; script: string; dispatch_namespace?: string }[]>(
218 "GET",
219 `/accounts/${this.account}/workers/durable_objects/namespaces?per_page=1000&page=${page}`,
220 );
221 const found = namespaces.find((n) => n.class === APP_CLASS && n.script === script && n.dispatch_namespace === this.namespace);
222 if (found) return found.id;
223 if (namespaces.length < 1000) break;
224 }
225 throw new Error(`The app's Durable Object namespace was not found for ${script}.`);
226 }
227
228 /** The Container application named `name`, if there is one. */
229 async containerApplication(name: string): Promise<{ id: string } | null> {
230 const apps = await this.call<{ id: string; name: string }[]>("GET", `/accounts/${this.account}/containers/applications`);
231 return apps.find((app) => app.name === name) ?? null;
232 }
233
234 /**
235 * Runs `image` at `instanceType` for the app `script`: its Container
236 * application is made the first time, and rolled out to after.
237 */
238 async runContainer(script: string, image: string, instanceType: string, description: string): Promise<string> {
239 const name = applicationName(script);
240 const existing = await this.containerApplication(name);
241 if (existing) {
242 await this.call(
243 "POST",
244 `/accounts/${this.account}/containers/applications/${existing.id}/rollouts`,
245 JSON.stringify(rolloutRequest(image, instanceType, description)),
246 "application/json",
247 );
248 return existing.id;
249 }
250 const namespaceId = await this.appNamespace(script);
251 const created = await this.call<{ id: string }>(
252 "POST",
253 `/accounts/${this.account}/containers/applications`,
254 JSON.stringify(applicationRequest(name, image, instanceType, namespaceId)),
255 "application/json",
256 );
257 return created.id;
258 }
259
260 /** Deletes the app's Container application, and its instances with it. Already gone is fine. */
261 async deleteContainer(script: string, id?: string | null): Promise<void> {
262 const found = id ? { id } : await this.containerApplication(applicationName(script)).catch(() => null);
263 if (!found) return;
264 try {
265 await this.call("DELETE", `/accounts/${this.account}/containers/applications/${found.id}`);
266 } catch (error) {
267 if (!/404|not found/i.test(String(error))) throw error;
268 }
269 }
270
What g1t can't do yet, and an open letter to Cloudflare271 /** Every app in the namespace, with when it was last changed. */
272 async listScripts(): Promise<{ id: string; modified_on: string }[]> {
273 return this.call<{ id: string; modified_on: string }[]>(
274 "GET",
275 `/accounts/${this.account}/workers/dispatch/namespaces/${this.namespace}/scripts`,
276 );
277 }
278
279 /**
280 * Takes an app down. Already gone is fine.
281 *
282 * A deleted app can keep answering for a while where Cloudflare still has
283 * it warm. So it is first replaced by a notice that it is down, which
284 * reaches the edge as fast as any deploy, and deleted after.
285 */
286 async deleteScript(script: string): Promise<void> {
287 await this.placeholder(script, TAKEN_DOWN, "taken-down").catch(() => undefined);
288 try {
289 await this.call("DELETE", `${this.scriptPath(script)}?force=true`);
290 } catch (error) {
291 if (!/404|not found|10007/i.test(String(error))) throw error;
292 }
293 }
294
295 /**
296 * Replaces an app with a notice that it is paused: its workspace reached
297 * its limit. The notice costs next to nothing to answer with, and the app
298 * comes back by being deployed again.
299 */
300 async pauseScript(script: string): Promise<void> {
301 await this.placeholder(script, PAUSED, "paused");
302 }
303
304 /**
305 * Replaces an app with a redirect to `targetHost`, path and query kept:
306 * what an app's old address answers after its workspace is renamed.
307 */
308 async redirectScript(script: string, targetHost: string): Promise<void> {
309 await this.placeholder(script, redirectScript(targetHost), "redirect");
310 }
311
312 private async placeholder(script: string, code: string, tag: string): Promise<void> {
313 const form = new FormData();
314 form.append(
315 "metadata",
316 JSON.stringify({ main_module: "index.js", compatibility_date: "2026-09-26", bindings: [], tags: [tag] }),
317 );
318 form.append("index.js", new File([code], "index.js", { type: "application/javascript+module" }));
319 await this.call("PUT", this.scriptPath(script), form);
320 }
321
322 /**
323 * Requests and CPU time per app over a period, from Workers analytics.
324 * Apps with no traffic are absent.
325 */
326 async usage(
327 scripts: string[],
328 since: string,
329 until: string,
330 ): Promise<Map<string, { requests: number; cpuMs: number }>> {
331 const totals = new Map<string, { requests: number; cpuMs: number }>();
332 if (scripts.length === 0) return totals;
333 const query = (withCpu: boolean) => `query ($account: string!, $since: Time!, $until: Time!, $scripts: [string!]) {
334 viewer { accounts(filter: { accountTag: $account }) {
335 workersInvocationsAdaptive(limit: 10000, filter: { datetime_geq: $since, datetime_lt: $until, scriptName_in: $scripts }) {
336 sum { requests${withCpu ? " cpuTimeUs" : ""} }
337 dimensions { scriptName }
338 }
339 } }
340 }`;
341 type Row = { sum: { requests: number; cpuTimeUs?: number }; dimensions: { scriptName: string } };
342 const ask = async (withCpu: boolean) => {
343 const response = await fetch(`${API}/graphql`, {
344 method: "POST",
345 headers: { authorization: `Bearer ${this.token}`, "content-type": "application/json" },
346 body: JSON.stringify({
347 query: query(withCpu),
348 variables: { account: this.account, since, until, scripts },
349 }),
350 });
351 return (await response.json()) as {
352 data?: { viewer: { accounts: { workersInvocationsAdaptive: Row[] }[] } };
353 errors?: { message: string }[] | null;
354 };
355 };
356 let answer = await ask(true);
357 // CPU time is counted where analytics offers it; requests always.
358 if (answer.errors?.length) answer = await ask(false);
359 if (answer.errors?.length || !answer.data) {
360 throw new Error(`Workers analytics refused: ${answer.errors?.map((e) => e.message).join("; ")}`);
361 }
362 for (const row of answer.data.viewer.accounts[0]?.workersInvocationsAdaptive ?? []) {
363 const seen = totals.get(row.dimensions.scriptName) ?? { requests: 0, cpuMs: 0 };
364 seen.requests += row.sum.requests;
365 seen.cpuMs += Math.ceil((row.sum.cpuTimeUs ?? 0) / 1000);
366 totals.set(row.dimensions.scriptName, seen);
367 }
368 return totals;
369 }
370}

This file's history is long; its oldest lines are credited to the oldest commit read.