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-guardrails | 1 | /** |
| 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 | |
| 9 | * is the one place to read or change them. docs/RATE-LIMITS.md explains | |
| 10 | * the choices; the docs' rate limits page shows the public ones. | |
| 11 | * | |
| 12 | * A limit fails open: no binding (self-hosted, `wrangler dev` without one) | |
| 13 | * or a binding that throws lets the request through. A limit guards | |
| 14 | * against floods; it is never a reason for g1t to stop answering. | |
| 15 | */ | |
| 16 | ||
| 17 | /** A Workers Rate Limiting binding, as the runtime hands it over. */ | |
| 18 | export type RateLimitBinding = { limit(options: { key: string }): Promise<{ success: boolean }> }; | |
| 19 | ||
| 20 | /** What asking a limit said. `unavailable` lets the request through. */ | |
| 21 | export type LimitVerdict = "allowed" | "limited" | "unavailable"; | |
| 22 | ||
| 23 | /** | |
| 24 | * Every limit's window, in seconds. Workers Rate Limiting counts over 10 | |
| 25 | * or 60 seconds; every g1t limit uses 60, and a client past one is told to | |
| 26 | * wait that long. | |
| 27 | */ | |
| 28 | export const LIMIT_PERIOD_SECONDS = 60; | |
| 29 | ||
| 30 | export type RateLimitSpec = { | |
| 31 | /** The Worker whose wrangler.jsonc declares it. */ | |
| 32 | worker: string; | |
| 33 | /** Unique per Cloudflare account; allocated in blocks per Worker (docs/RATE-LIMITS.md). */ | |
| 34 | namespaceId: number; | |
| 35 | /** Requests per `LIMIT_PERIOD_SECONDS` per key. */ | |
| 36 | limit: number; | |
| 37 | /** What a key is. */ | |
| 38 | per: string; | |
| 39 | }; | |
| 40 | ||
| 41 | /** | |
| 42 | * Every rate limit binding, by name. Generous for people, tight for floods: | |
| 43 | * a person browsing makes a few requests a second at most, a clone is three | |
| 44 | * git requests, and nothing a person does needs dozens of archives a minute. | |
| 45 | * Namespace ids go in blocks of a hundred per Worker: 41xx packages, | |
| 46 | * 42xx web, 43xx repos, 44xx api, 45xx og, 46xx status. | |
| 47 | */ | |
| 48 | export const RATE_LIMITS = { | |
| 49 | // services/packages (src/limits.rs): registry pulls and token requests. | |
| 50 | ANONYMOUS_LIMIT: { worker: "services/packages", namespaceId: 4101, limit: 300, per: "address" }, | |
| 51 | SIGNED_LIMIT: { worker: "services/packages", namespaceId: 4102, limit: 5000, per: "person, workspace or agent" }, | |
| 52 | // apps/web (workers/app.ts): the front door. | |
| 53 | WEB_ANONYMOUS_LIMIT: { worker: "apps/web", namespaceId: 4201, limit: 600, per: "address, signed-out pages" }, | |
| 54 | WEB_HEAVY_LIMIT: { worker: "apps/web", namespaceId: 4202, limit: 30, per: "address, signed-out archives, run pages, logs and search" }, | |
| 55 | WEB_SESSION_LIMIT: { worker: "apps/web", namespaceId: 4203, limit: 1200, per: "session, signed-in requests" }, | |
| 56 | WEB_ADDRESS_LIMIT: { worker: "apps/web", namespaceId: 4204, limit: 3000, per: "address, every request that reaches the Worker" }, | |
| 57 | GIT_ANONYMOUS_LIMIT: { worker: "apps/web", namespaceId: 4205, limit: 120, per: "address, git requests without credentials" }, | |
| 58 | GIT_SIGNED_LIMIT: { worker: "apps/web", namespaceId: 4206, limit: 1200, per: "credential, git requests with credentials" }, | |
| Merge branch 'worktree-agent-ad4439ce85a91ecb4' into integrate | 59 | // The same as API_TOKEN_LIMIT: a token counts alike on the website and the API. |
| 60 | 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-guardrails | 61 | // services/repos (src/lib.rs): what an anonymous clone can cost a repository's owner. |
| 62 | PACK_FILL_LIMIT: { worker: "services/repos", namespaceId: 4301, limit: 30, per: "repository, packs written to the pack cache" }, | |
| 63 | ANONYMOUS_FETCH_LIMIT: { worker: "services/repos", namespaceId: 4302, limit: 120, per: "repository, anonymous fetches the store answers" }, | |
| 64 | // apps/api (src/limits.rs): REST and MCP, which count apart. | |
| 65 | API_ANONYMOUS_LIMIT: { worker: "apps/api", namespaceId: 4401, limit: 60, per: "address, requests without a token" }, | |
| 66 | API_TOKEN_LIMIT: { worker: "apps/api", namespaceId: 4402, limit: 1000, per: "token" }, | |
| 67 | // services/og: drawing a card that is not in the cache. | |
| 68 | OG_RENDER_LIMIT: { worker: "services/og", namespaceId: 4501, limit: 60, per: "address, cards drawn" }, | |
| 69 | // apps/status: asking for a subscription, which sends an email. | |
| 70 | STATUS_SUBSCRIBE_LIMIT: { worker: "apps/status", namespaceId: 4601, limit: 3, per: "address" }, | |
| 71 | STATUS_EMAIL_LIMIT: { worker: "apps/status", namespaceId: 4602, limit: 2, per: "email address" }, | |
| 72 | } as const satisfies Record<string, RateLimitSpec>; | |
| 73 | ||
| 74 | export type RateLimitName = keyof typeof RATE_LIMITS; | |
| 75 | ||
| 76 | /** | |
| 77 | * Counts one request against `binding` under `key`. Never throws: a missing | |
| 78 | * binding or one that fails is `unavailable`, which callers let through. | |
| 79 | */ | |
| 80 | export async function checkLimit(binding: RateLimitBinding | undefined, key: string): Promise<LimitVerdict> { | |
| 81 | if (!binding) return "unavailable"; | |
| 82 | try { | |
| 83 | const { success } = await binding.limit({ key }); | |
| 84 | return success ? "allowed" : "limited"; | |
| 85 | } catch (error) { | |
| 86 | console.error(JSON.stringify({ event: "rate_limit.unavailable", error: String(error) })); | |
| 87 | return "unavailable"; | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | /** Whether the request is past its limit. Fails open, like `checkLimit`. */ | |
| 92 | export async function isLimited(binding: RateLimitBinding | undefined, key: string): Promise<boolean> { | |
| 93 | return (await checkLimit(binding, key)) === "limited"; | |
| 94 | } | |
| 95 | ||
| 96 | /** The client's address as Cloudflare saw it, for keys; `unknown` when there is none (local dev). */ | |
| 97 | export function clientAddress(request: Request): string { | |
| 98 | return request.headers.get("cf-connecting-ip")?.trim() || "unknown"; | |
| 99 | } | |
| 100 | ||
| 101 | /** | |
| 102 | * A key for a secret (a session cookie, a token, a credential): `prefix:` | |
| 103 | * and the first 16 hex digits of its SHA-256, so the secret itself never | |
| 104 | * reaches the rate limiter. | |
| 105 | */ | |
| 106 | export async function secretKey(prefix: string, secret: string): Promise<string> { | |
| 107 | const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret))); | |
| 108 | let hex = ""; | |
| 109 | for (const byte of digest.subarray(0, 8)) hex += byte.toString(16).padStart(2, "0"); | |
| 110 | return `${prefix}:${hex}`; | |
| 111 | } | |
| 112 | ||
| 113 | /** A 429 with `Retry-After`, as plain text unless `headers` says otherwise. */ | |
| 114 | export function tooManyRequests(body: string, headers: Record<string, string> = {}): Response { | |
| 115 | return new Response(body, { | |
| 116 | status: 429, | |
| 117 | headers: { | |
| 118 | "content-type": "text/plain; charset=utf-8", | |
| 119 | "retry-after": String(LIMIT_PERIOD_SECONDS), | |
| 120 | "cache-control": "no-store", | |
| 121 | ...headers, | |
| 122 | }, | |
| 123 | }); | |
| 124 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.