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 | ||
| Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address | 14 | import { gitLimited, heavy, pageLimited, sessionCookie, unlimited, usercontentLimited } from "./front-door-limits.ts"; |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 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 | ||
| Merge main into Artifacts Phase 2 | 114 | test("requests with an access token count by a hash of the token, at the API's limit", async () => { |
| 115 | const env = { WEB_ADDRESS_LIMIT: binding(100), WEB_TOKEN_LIMIT: binding(1), WEB_SESSION_LIMIT: binding(100), WEB_ANONYMOUS_LIMIT: binding(0) }; | |
| 116 | const withToken = () => request("/acme/rocket", { authorization: "Bearer g1t_secret", cookie: "g1t_session=abc123" }); | |
| 117 | assert.equal(await pageLimited(env, withToken(), "/acme/rocket"), null); | |
| 118 | assert.equal(env.WEB_SESSION_LIMIT.keys.length, 0, "the token counts, not a cookie beside it"); | |
| 119 | assert.equal(env.WEB_ANONYMOUS_LIMIT.keys.length, 0); | |
| 120 | assert.match(env.WEB_TOKEN_LIMIT.keys[0]!, /^token:[0-9a-f]{16}$/); | |
| 121 | assert.ok(!env.WEB_TOKEN_LIMIT.keys[0]!.includes("g1t_secret")); | |
| 122 | const refused = await pageLimited(env, withToken(), "/acme/rocket"); | |
| 123 | assert.equal(refused?.status, 429); | |
| 124 | assert.match((await refused?.text()) ?? "", /this access token/); | |
| 125 | assert.equal(RATE_LIMITS.WEB_TOKEN_LIMIT.limit, RATE_LIMITS.API_TOKEN_LIMIT.limit); | |
| 126 | }); | |
| 127 | ||
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 128 | test("files the Worker serves itself are never limited", async () => { |
| 129 | const env = { WEB_ADDRESS_LIMIT: binding(0), WEB_ANONYMOUS_LIMIT: binding(0) }; | |
| 130 | for (const path of ["/assets/app-1a2b.js", "/fonts/hanken.woff2", "/favicon.ico", "/robots.txt", "/llms.txt", "/sitemap.xml"]) { | |
| 131 | assert.ok(unlimited(path), path); | |
| 132 | assert.equal(await pageLimited(env, request(path), path), null, path); | |
| 133 | } | |
| 134 | assert.ok(!unlimited("/acme/rocket/blob/main/logo.png"), "a file in a repository is a page"); | |
| 135 | }); | |
| 136 | ||
| 137 | test("costly pages are recognised", () => { | |
| 138 | for (const path of [ | |
| 139 | "/search", | |
| 140 | "/search.data", | |
| 141 | "/acme/rocket/archive/main.zip", | |
| 142 | "/acme/rocket/actions/runs/run_1", | |
| 143 | "/acme/rocket/actions/runs/run_1.data", | |
| 144 | "/acme/rocket/actions/runs/run_1/logs.zip", | |
| 145 | "/acme/rocket/actions/runs/run_1/artifacts/dist", | |
| 146 | "/acme/rocket/actions/jobs/job_1/log.txt", | |
| A repository's Security overview is one row per feature in three groups, each with a status word (Enabled, Disabled, Needs setup, Coming), a line on what it does and the one thing to do about it: the security policy (View policy, or Set up a security policy, which commits a starting SECURITY.md on a branch and opens it as a pull request), dependency alerts, security updates (the switch, kept on Vulnerabilities and now in Settings too), code scanning (Enabled once an analysis was uploaded), secret scanning (push protection always on) and code quality as Coming; and under Reporting, advisories and the private vulnerability reporting switch. Anyone who can see a public repository but has no role on it sees the public face at the same address: the policy, the published advisories and, when it is on, Report a vulnerability. Advisories get their pages (the list with its state filter, one advisory with Publish, Withdraw and Edit, a form for a new draft or changes) and reports theirs (new ones first, each accepted into a draft advisory or declined), with a Reports page beside Advisories and an Advisories tab; the public report form takes an email when signed out and counts against the front door's heavy limit. Security settings gain Alerts for secrets in the history and Private vulnerability reporting. Settings → Notifications gains a Security section: critical and high alerts at once (always, for the workspaces one owns or manages security for) and the rest in a daily or weekly digest, at once, or not at all, with security alerts now among the email reasons. The notifications guide says how security alerts arrive and what the digest is, the security guide the overview's rows and the public face, secret protection the history alerts switch, webhooks the summary and advisory events, and a new guide covers advisories and private reporting; the report_settings method lets the public page know whether a repository takes reports. | 147 | "/acme/rocket/security/report", |
| 148 | "/acme/rocket/security/report.data", | |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 149 | ]) { |
| 150 | assert.ok(heavy(path), path); | |
| 151 | } | |
| A repository's Security overview is one row per feature in three groups, each with a status word (Enabled, Disabled, Needs setup, Coming), a line on what it does and the one thing to do about it: the security policy (View policy, or Set up a security policy, which commits a starting SECURITY.md on a branch and opens it as a pull request), dependency alerts, security updates (the switch, kept on Vulnerabilities and now in Settings too), code scanning (Enabled once an analysis was uploaded), secret scanning (push protection always on) and code quality as Coming; and under Reporting, advisories and the private vulnerability reporting switch. Anyone who can see a public repository but has no role on it sees the public face at the same address: the policy, the published advisories and, when it is on, Report a vulnerability. Advisories get their pages (the list with its state filter, one advisory with Publish, Withdraw and Edit, a form for a new draft or changes) and reports theirs (new ones first, each accepted into a draft advisory or declined), with a Reports page beside Advisories and an Advisories tab; the public report form takes an email when signed out and counts against the front door's heavy limit. Security settings gain Alerts for secrets in the history and Private vulnerability reporting. Settings → Notifications gains a Security section: critical and high alerts at once (always, for the workspaces one owns or manages security for) and the rest in a daily or weekly digest, at once, or not at all, with security alerts now among the email reasons. The notifications guide says how security alerts arrive and what the digest is, the security guide the overview's rows and the public face, secret protection the history alerts switch, webhooks the summary and advisory events, and a new guide covers advisories and private reporting; the report_settings method lets the public page know whether a repository takes reports. | 152 | for (const path of ["/", "/acme/rocket", "/acme/rocket/actions", "/acme/rocket/issues/1", "/acme/search", "/acme/rocket/security", "/acme/rocket/security/reports"]) { |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 153 | assert.ok(!heavy(path), path); |
| 154 | } | |
| 155 | }); | |
| 156 | ||
| 157 | test("the session cookie is read from among others", () => { | |
| 158 | assert.equal(sessionCookie("theme=dark; g1t_session=abc; x=1"), "abc"); | |
| 159 | assert.equal(sessionCookie("g1t_session=abc"), "abc"); | |
| 160 | assert.equal(sessionCookie("not_g1t_session=abc"), null); | |
| 161 | assert.equal(sessionCookie(null), null); | |
| 162 | }); | |
| 163 | ||
| 164 | /** A wrangler.jsonc as JSON: comments and trailing commas out, strings kept. */ | |
| 165 | function readJsonc(path: string): { ratelimits?: { name: string; namespace_id: string; simple: { limit: number; period: number } }[] } { | |
| 166 | const text = readFileSync(new URL(path, import.meta.url), "utf8") | |
| 167 | .replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, string: string | undefined) => string ?? "") | |
| 168 | .replace(/,(\s*[}\]])/g, "$1"); | |
| 169 | return JSON.parse(text); | |
| 170 | } | |
| 171 | ||
| 172 | test("every wrangler.jsonc declares the rate limits RATE_LIMITS lists, and only those", () => { | |
| 173 | const ids = new Set<number>(); | |
| 174 | const workers = new Set(Object.values(RATE_LIMITS).map((spec) => spec.worker)); | |
| 175 | for (const worker of workers) { | |
| 176 | const declared = readJsonc(`../../../../${worker}/wrangler.jsonc`).ratelimits ?? []; | |
| 177 | const listed = Object.entries(RATE_LIMITS).filter(([, spec]) => spec.worker === worker); | |
| 178 | assert.deepEqual( | |
| 179 | declared.map((binding) => binding.name).sort(), | |
| 180 | listed.map(([name]) => name).sort(), | |
| 181 | `${worker}'s bindings`, | |
| 182 | ); | |
| 183 | for (const [name, spec] of listed) { | |
| 184 | const binding = declared.find((b) => b.name === name)!; | |
| 185 | assert.equal(Number(binding.namespace_id), spec.namespaceId, `${name}'s namespace id`); | |
| 186 | assert.equal(binding.simple.limit, spec.limit, `${name}'s limit`); | |
| 187 | assert.equal(binding.simple.period, LIMIT_PERIOD_SECONDS, `${name}'s period`); | |
| 188 | } | |
| 189 | } | |
| 190 | for (const spec of Object.values(RATE_LIMITS)) { | |
| 191 | assert.ok(!ids.has(spec.namespaceId), `namespace id ${spec.namespaceId} is used once`); | |
| 192 | ids.add(spec.namespaceId); | |
| 193 | } | |
| 194 | }); | |
| Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address | 195 | |
| 196 | test("repository files on the usercontent origin count per address; avatars do not", async () => { | |
| 197 | const env = { WEB_ANONYMOUS_LIMIT: binding(1) }; | |
| 198 | const raw = "/acme/rocket/raw/main/logo.png"; | |
| 199 | assert.equal(await usercontentLimited(env, request(raw), raw), null); | |
| 200 | assert.equal((await usercontentLimited(env, request(raw), raw))?.status, 429); | |
| 201 | assert.equal(await usercontentLimited(env, request("/avatars/" + "a".repeat(64)), "/avatars/" + "a".repeat(64)), null); | |
| 202 | assert.deepEqual(env.WEB_ANONYMOUS_LIMIT.keys, ["ip:203.0.113.9", "ip:203.0.113.9"]); | |
| 203 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.