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 | import assert from "node:assert/strict"; |
| 2 | import { readFileSync } from "node:fs"; | |
| 3 | import { test } from "node:test"; | |
| 4 | ||
| 5 | import { | |
| 6 | LIMIT_PERIOD_SECONDS, | |
| 7 | RATE_LIMITS, | |
| 8 | type RateLimitBinding, | |
| 9 | checkLimit, | |
| 10 | isLimited, | |
| 11 | secretKey, | |
| 12 | } from "@g1t/contracts/rate-limits"; | |
| 13 | ||
| 14 | import { gitLimited, heavy, pageLimited, sessionCookie, unlimited } from "./front-door-limits.ts"; | |
| 15 | ||
| 16 | /** A binding that lets `allow` requests through per key, recording each key it was asked. */ | |
| 17 | function binding(allow: number): RateLimitBinding & { keys: string[] } { | |
| 18 | const counts = new Map<string, number>(); | |
| 19 | const keys: string[] = []; | |
| 20 | return { | |
| 21 | keys, | |
| 22 | async limit({ key }) { | |
| 23 | keys.push(key); | |
| 24 | const count = (counts.get(key) ?? 0) + 1; | |
| 25 | counts.set(key, count); | |
| 26 | return { success: count <= allow }; | |
| 27 | }, | |
| 28 | }; | |
| 29 | } | |
| 30 | ||
| 31 | const broken: RateLimitBinding = { | |
| 32 | async limit() { | |
| 33 | throw new Error("the binding is down"); | |
| 34 | }, | |
| 35 | }; | |
| 36 | ||
| 37 | function request(path: string, headers: Record<string, string> = {}): Request { | |
| 38 | return new Request(`https://g1t.sh${path}`, { headers: { "cf-connecting-ip": "203.0.113.9", ...headers } }); | |
| 39 | } | |
| 40 | ||
| 41 | test("a limit lets requests through until it is reached", async () => { | |
| 42 | const limit = binding(2); | |
| 43 | assert.equal(await checkLimit(limit, "ip:1"), "allowed"); | |
| 44 | assert.equal(await checkLimit(limit, "ip:1"), "allowed"); | |
| 45 | assert.equal(await checkLimit(limit, "ip:1"), "limited"); | |
| 46 | assert.equal(await checkLimit(limit, "ip:2"), "allowed", "each key counts apart"); | |
| 47 | }); | |
| 48 | ||
| 49 | test("a missing or failing binding fails open", async () => { | |
| 50 | const logged = console.error; | |
| 51 | console.error = () => {}; | |
| 52 | try { | |
| 53 | assert.equal(await checkLimit(undefined, "ip:1"), "unavailable"); | |
| 54 | assert.equal(await checkLimit(broken, "ip:1"), "unavailable"); | |
| 55 | assert.equal(await isLimited(broken, "ip:1"), false); | |
| 56 | assert.equal(await gitLimited({ GIT_ANONYMOUS_LIMIT: broken }, request("/acme/rocket.git/info/refs")), null); | |
| 57 | assert.equal(await pageLimited({ WEB_ADDRESS_LIMIT: broken, WEB_ANONYMOUS_LIMIT: broken }, request("/acme/rocket"), "/acme/rocket"), null); | |
| 58 | } finally { | |
| 59 | console.error = logged; | |
| 60 | } | |
| 61 | }); | |
| 62 | ||
| 63 | test("secrets are keyed by a short hash, never as they are", async () => { | |
| 64 | const key = await secretKey("session", "s3cret-session"); | |
| 65 | assert.match(key, /^session:[0-9a-f]{16}$/); | |
| 66 | assert.equal(await secretKey("session", "s3cret-session"), key); | |
| 67 | assert.notEqual(await secretKey("session", "another"), key); | |
| 68 | }); | |
| 69 | ||
| 70 | test("git without credentials is limited by address, with a message git shows", async () => { | |
| 71 | const env = { GIT_ANONYMOUS_LIMIT: binding(1), GIT_SIGNED_LIMIT: binding(1) }; | |
| 72 | const clone = () => request("/acme/rocket.git/info/refs"); | |
| 73 | assert.equal(await gitLimited(env, clone()), null); | |
| 74 | const refused = await gitLimited(env, clone()); | |
| 75 | assert.equal(refused?.status, 429); | |
| 76 | assert.equal(refused?.headers.get("retry-after"), String(LIMIT_PERIOD_SECONDS)); | |
| 77 | assert.match(refused?.headers.get("content-type") ?? "", /^text\/plain/); | |
| 78 | assert.match((await refused?.text()) ?? "", /Too many git requests/); | |
| 79 | assert.deepEqual(env.GIT_ANONYMOUS_LIMIT.keys, ["ip:203.0.113.9", "ip:203.0.113.9"]); | |
| 80 | }); | |
| 81 | ||
| 82 | test("git with credentials counts by a hash of them, apart from the address", async () => { | |
| 83 | const env = { GIT_ANONYMOUS_LIMIT: binding(0), GIT_SIGNED_LIMIT: binding(5) }; | |
| 84 | const authorization = `Basic ${btoa("ada:g1t_token")}`; | |
| 85 | assert.equal(await gitLimited(env, request("/acme/rocket.git/git-upload-pack", { authorization })), null); | |
| 86 | assert.equal(env.GIT_ANONYMOUS_LIMIT.keys.length, 0); | |
| 87 | assert.match(env.GIT_SIGNED_LIMIT.keys[0]!, /^git:[0-9a-f]{16}$/); | |
| 88 | assert.ok(!env.GIT_SIGNED_LIMIT.keys[0]!.includes("g1t_token")); | |
| 89 | }); | |
| 90 | ||
| 91 | test("signed-out pages count by address, and costly ones against a tighter limit too", async () => { | |
| 92 | const env = { WEB_ADDRESS_LIMIT: binding(100), WEB_ANONYMOUS_LIMIT: binding(100), WEB_HEAVY_LIMIT: binding(1) }; | |
| 93 | assert.equal(await pageLimited(env, request("/acme/rocket"), "/acme/rocket"), null); | |
| 94 | assert.equal(env.WEB_HEAVY_LIMIT.keys.length, 0); | |
| 95 | const archive = "/acme/rocket/archive/main.zip"; | |
| 96 | assert.equal(await pageLimited(env, request(archive), archive), null); | |
| 97 | const refused = await pageLimited(env, request(archive), archive); | |
| 98 | assert.equal(refused?.status, 429); | |
| 99 | assert.match((await refused?.text()) ?? "", /Signed-in accounts have a higher limit/); | |
| 100 | assert.equal(await pageLimited(env, request("/acme/rocket/issues"), "/acme/rocket/issues"), null, "other pages go on"); | |
| 101 | }); | |
| 102 | ||
| 103 | test("signed-in requests count by session, and every request by address", async () => { | |
| 104 | const env = { WEB_ADDRESS_LIMIT: binding(1), WEB_SESSION_LIMIT: binding(100), WEB_ANONYMOUS_LIMIT: binding(0) }; | |
| 105 | const signedIn = () => request("/acme/rocket/archive/main.zip", { cookie: "theme=dark; g1t_session=abc123" }); | |
| 106 | assert.equal(await pageLimited(env, signedIn(), "/acme/rocket/archive/main.zip"), null); | |
| 107 | assert.equal(env.WEB_ANONYMOUS_LIMIT.keys.length, 0); | |
| 108 | assert.match(env.WEB_SESSION_LIMIT.keys[0]!, /^session:[0-9a-f]{16}$/); | |
| 109 | // Made-up cookies still meet the ceiling per address. | |
| 110 | const refused = await pageLimited(env, request("/", { cookie: "g1t_session=made-up" }), "/"); | |
| 111 | assert.equal(refused?.status, 429); | |
| 112 | }); | |
| 113 | ||
| 114 | test("files the Worker serves itself are never limited", async () => { | |
| 115 | const env = { WEB_ADDRESS_LIMIT: binding(0), WEB_ANONYMOUS_LIMIT: binding(0) }; | |
| 116 | for (const path of ["/assets/app-1a2b.js", "/fonts/hanken.woff2", "/favicon.ico", "/robots.txt", "/llms.txt", "/sitemap.xml"]) { | |
| 117 | assert.ok(unlimited(path), path); | |
| 118 | assert.equal(await pageLimited(env, request(path), path), null, path); | |
| 119 | } | |
| 120 | assert.ok(!unlimited("/acme/rocket/blob/main/logo.png"), "a file in a repository is a page"); | |
| 121 | }); | |
| 122 | ||
| 123 | test("costly pages are recognised", () => { | |
| 124 | for (const path of [ | |
| 125 | "/search", | |
| 126 | "/search.data", | |
| 127 | "/acme/rocket/archive/main.zip", | |
| 128 | "/acme/rocket/actions/runs/run_1", | |
| 129 | "/acme/rocket/actions/runs/run_1.data", | |
| 130 | "/acme/rocket/actions/runs/run_1/logs.zip", | |
| 131 | "/acme/rocket/actions/runs/run_1/artifacts/dist", | |
| 132 | "/acme/rocket/actions/jobs/job_1/log.txt", | |
| 133 | ]) { | |
| 134 | assert.ok(heavy(path), path); | |
| 135 | } | |
| 136 | for (const path of ["/", "/acme/rocket", "/acme/rocket/actions", "/acme/rocket/issues/1", "/acme/search"]) { | |
| 137 | assert.ok(!heavy(path), path); | |
| 138 | } | |
| 139 | }); | |
| 140 | ||
| 141 | test("the session cookie is read from among others", () => { | |
| 142 | assert.equal(sessionCookie("theme=dark; g1t_session=abc; x=1"), "abc"); | |
| 143 | assert.equal(sessionCookie("g1t_session=abc"), "abc"); | |
| 144 | assert.equal(sessionCookie("not_g1t_session=abc"), null); | |
| 145 | assert.equal(sessionCookie(null), null); | |
| 146 | }); | |
| 147 | ||
| 148 | /** A wrangler.jsonc as JSON: comments and trailing commas out, strings kept. */ | |
| 149 | function readJsonc(path: string): { ratelimits?: { name: string; namespace_id: string; simple: { limit: number; period: number } }[] } { | |
| 150 | const text = readFileSync(new URL(path, import.meta.url), "utf8") | |
| 151 | .replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, string: string | undefined) => string ?? "") | |
| 152 | .replace(/,(\s*[}\]])/g, "$1"); | |
| 153 | return JSON.parse(text); | |
| 154 | } | |
| 155 | ||
| 156 | test("every wrangler.jsonc declares the rate limits RATE_LIMITS lists, and only those", () => { | |
| 157 | const ids = new Set<number>(); | |
| 158 | const workers = new Set(Object.values(RATE_LIMITS).map((spec) => spec.worker)); | |
| 159 | for (const worker of workers) { | |
| 160 | const declared = readJsonc(`../../../../${worker}/wrangler.jsonc`).ratelimits ?? []; | |
| 161 | const listed = Object.entries(RATE_LIMITS).filter(([, spec]) => spec.worker === worker); | |
| 162 | assert.deepEqual( | |
| 163 | declared.map((binding) => binding.name).sort(), | |
| 164 | listed.map(([name]) => name).sort(), | |
| 165 | `${worker}'s bindings`, | |
| 166 | ); | |
| 167 | for (const [name, spec] of listed) { | |
| 168 | const binding = declared.find((b) => b.name === name)!; | |
| 169 | assert.equal(Number(binding.namespace_id), spec.namespaceId, `${name}'s namespace id`); | |
| 170 | assert.equal(binding.simple.limit, spec.limit, `${name}'s limit`); | |
| 171 | assert.equal(binding.simple.period, LIMIT_PERIOD_SECONDS, `${name}'s period`); | |
| 172 | } | |
| 173 | } | |
| 174 | for (const spec of Object.values(RATE_LIMITS)) { | |
| 175 | assert.ok(!ids.has(spec.namespaceId), `namespace id ${spec.namespaceId} is used once`); | |
| 176 | ids.add(spec.namespaceId); | |
| 177 | } | |
| 178 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.