Skip to content
125 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.

Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails1/**
2 * Rate limits on g1t's public surfaces: Workers Rate Limiting bindings
3 * (`ratelimits` in each wrangler.jsonc), asked once per request with a key:
4 * the client's address, or a hash of its session or token, never the
5 * secret itself.
6 *
7 * Every binding is listed in `RATE_LIMITS` with its namespace id and limit,
8 * and apps/web's tests check each wrangler.jsonc against it, so this table
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.9 * is the one place to read or change them. CONTRIBUTING.md, "Rate
10 * limits", says how; the docs' rate limits page
11 * (docs.g1t.sh/reference/rate-limits/) shows the public ones.
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails12 *
13 * A limit fails open: no binding (self-hosted, `wrangler dev` without one)
14 * or a binding that throws lets the request through. A limit guards
15 * against floods; it is never a reason for g1t to stop answering.
16 */
17
18/** A Workers Rate Limiting binding, as the runtime hands it over. */
19export type RateLimitBinding = { limit(options: { key: string }): Promise<{ success: boolean }> };
20
21/** What asking a limit said. `unavailable` lets the request through. */
22export type LimitVerdict = "allowed" | "limited" | "unavailable";
23
24/**
25 * Every limit's window, in seconds. Workers Rate Limiting counts over 10
26 * or 60 seconds; every g1t limit uses 60, and a client past one is told to
27 * wait that long.
28 */
29export const LIMIT_PERIOD_SECONDS = 60;
30
31export type RateLimitSpec = {
32 /** The Worker whose wrangler.jsonc declares it. */
33 worker: string;
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.34 /** Unique per Cloudflare account; allocated in blocks per Worker (CONTRIBUTING.md, "Rate limits"). */
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails35 namespaceId: number;
36 /** Requests per `LIMIT_PERIOD_SECONDS` per key. */
37 limit: number;
38 /** What a key is. */
39 per: string;
40};
41
42/**
43 * Every rate limit binding, by name. Generous for people, tight for floods:
44 * a person browsing makes a few requests a second at most, a clone is three
45 * git requests, and nothing a person does needs dozens of archives a minute.
46 * Namespace ids go in blocks of a hundred per Worker: 41xx packages,
47 * 42xx web, 43xx repos, 44xx api, 45xx og, 46xx status.
48 */
49export const RATE_LIMITS = {
50 // services/packages (src/limits.rs): registry pulls and token requests.
51 ANONYMOUS_LIMIT: { worker: "services/packages", namespaceId: 4101, limit: 300, per: "address" },
52 SIGNED_LIMIT: { worker: "services/packages", namespaceId: 4102, limit: 5000, per: "person, workspace or agent" },
53 // apps/web (workers/app.ts): the front door.
54 WEB_ANONYMOUS_LIMIT: { worker: "apps/web", namespaceId: 4201, limit: 600, per: "address, signed-out pages" },
A running job's log is live, as on GitHub. A step's output reaches its run's page within about two seconds: the runner sends what is printed every second, and the page asks every second while a step runs, every three while none does; an answer that fails (a 429, a 5xx, the network) is asked again after a wait that doubles to half a minute, or as long as Retry-After says, where before one such answer stopped the log for good and the step said "No output yet." until the page was reloaded, and a signed-out viewer's run page, refreshing beside its log, ran into the 30-a-minute limit within a minute; the live log now counts as a page, and only log downloads as costly. A hidden tab stops asking and catches up when shown, and the page says when it is reconnecting. The running step opens by itself, its timer counts, its log follows the newest line while its end is on screen and offers Jump to latest once the viewer scrolls up, a step that has printed nothing for a while says "Running · no output for 2m 10s", and the job's header names the step it is on. Colour that tools print (bold, dim, italic, underline, the 16 colours as theme tokens for light and dark, 256 colours and 24-bit colour, text and background) is drawn as printed, other escape codes are dropped and links keep their text, and search reads the plain text and keeps the colours around what it marks. Every line is stamped with the time it was printed, as GitHub's raw logs are, so log.txt and the run's zip have a time at the start of each line, and a clock button beside Search logs shows the times, remembered by the browser. Set up job now gives the runner's version and name, the operating system, the runner image, the machine's architecture, CPUs and memory, the token's permissions under G1T_TOKEN Permissions, the secret source and the actions it downloads; each run step opens with GitHub's folded Run group, the script in cyan, the shell's whole path and the step's env, each uses step with its with inputs (an action's defaults among them) and env; cache and artifact transfers say how far they have got every five seconds; and a last Complete job step sets the job's outputs and stops what the steps left running, found by RUNNER_TRACKING_ID as GitHub's runner finds it. Two batches of a job's log written at once can no longer reach for the same seq and lose one. The Actions guide and the rate limits page say so.55 WEB_HEAVY_LIMIT: { worker: "apps/web", namespaceId: 4202, limit: 30, per: "address, signed-out archives, run pages, log downloads and search" },
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails56 WEB_SESSION_LIMIT: { worker: "apps/web", namespaceId: 4203, limit: 1200, per: "session, signed-in requests" },
57 WEB_ADDRESS_LIMIT: { worker: "apps/web", namespaceId: 4204, limit: 3000, per: "address, every request that reaches the Worker" },
58 GIT_ANONYMOUS_LIMIT: { worker: "apps/web", namespaceId: 4205, limit: 120, per: "address, git requests without credentials" },
59 GIT_SIGNED_LIMIT: { worker: "apps/web", namespaceId: 4206, limit: 1200, per: "credential, git requests with credentials" },
Merge main into Artifacts Phase 260 // The same as API_TOKEN_LIMIT: a token counts alike on the website and the API.
61 WEB_TOKEN_LIMIT: { worker: "apps/web", namespaceId: 4207, limit: 1000, per: "token, pages and data requests with an access token" },
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails62 // services/repos (src/lib.rs): what an anonymous clone can cost a repository's owner.
63 PACK_FILL_LIMIT: { worker: "services/repos", namespaceId: 4301, limit: 30, per: "repository, packs written to the pack cache" },
64 ANONYMOUS_FETCH_LIMIT: { worker: "services/repos", namespaceId: 4302, limit: 120, per: "repository, anonymous fetches the store answers" },
65 // apps/api (src/limits.rs): REST and MCP, which count apart.
66 API_ANONYMOUS_LIMIT: { worker: "apps/api", namespaceId: 4401, limit: 60, per: "address, requests without a token" },
67 API_TOKEN_LIMIT: { worker: "apps/api", namespaceId: 4402, limit: 1000, per: "token" },
68 // services/og: drawing a card that is not in the cache.
69 OG_RENDER_LIMIT: { worker: "services/og", namespaceId: 4501, limit: 60, per: "address, cards drawn" },
70 // apps/status: asking for a subscription, which sends an email.
71 STATUS_SUBSCRIBE_LIMIT: { worker: "apps/status", namespaceId: 4601, limit: 3, per: "address" },
72 STATUS_EMAIL_LIMIT: { worker: "apps/status", namespaceId: 4602, limit: 2, per: "email address" },
73} as const satisfies Record<string, RateLimitSpec>;
74
75export type RateLimitName = keyof typeof RATE_LIMITS;
76
77/**
78 * Counts one request against `binding` under `key`. Never throws: a missing
79 * binding or one that fails is `unavailable`, which callers let through.
80 */
81export async function checkLimit(binding: RateLimitBinding | undefined, key: string): Promise<LimitVerdict> {
82 if (!binding) return "unavailable";
83 try {
84 const { success } = await binding.limit({ key });
85 return success ? "allowed" : "limited";
86 } catch (error) {
87 console.error(JSON.stringify({ event: "rate_limit.unavailable", error: String(error) }));
88 return "unavailable";
89 }
90}
91
92/** Whether the request is past its limit. Fails open, like `checkLimit`. */
93export async function isLimited(binding: RateLimitBinding | undefined, key: string): Promise<boolean> {
94 return (await checkLimit(binding, key)) === "limited";
95}
96
97/** The client's address as Cloudflare saw it, for keys; `unknown` when there is none (local dev). */
98export function clientAddress(request: Request): string {
99 return request.headers.get("cf-connecting-ip")?.trim() || "unknown";
100}
101
102/**
103 * A key for a secret (a session cookie, a token, a credential): `prefix:`
104 * and the first 16 hex digits of its SHA-256, so the secret itself never
105 * reaches the rate limiter.
106 */
107export async function secretKey(prefix: string, secret: string): Promise<string> {
108 const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)));
109 let hex = "";
110 for (const byte of digest.subarray(0, 8)) hex += byte.toString(16).padStart(2, "0");
111 return `${prefix}:${hex}`;
112}
113
114/** A 429 with `Retry-After`, as plain text unless `headers` says otherwise. */
115export function tooManyRequests(body: string, headers: Record<string, string> = {}): Response {
116 return new Response(body, {
117 status: 429,
118 headers: {
119 "content-type": "text/plain; charset=utf-8",
120 "retry-after": String(LIMIT_PERIOD_SECONDS),
121 "cache-control": "no-store",
122 ...headers,
123 },
124 });
125}

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