Skip to content
203 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-guardrails1import assert from "node:assert/strict";
2import { readFileSync } from "node:fs";
3import { test } from "node:test";
4
5import {
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 address14import { gitLimited, heavy, pageLimited, sessionCookie, unlimited, usercontentLimited } from "./front-door-limits.ts";
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails15
16/** A binding that lets `allow` requests through per key, recording each key it was asked. */
17function 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
31const broken: RateLimitBinding = {
32 async limit() {
33 throw new Error("the binding is down");
34 },
35};
36
37function 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
41test("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
49test("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
63test("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
70test("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
82test("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
91test("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
103test("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 2114test("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-guardrails128test("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
137test("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-guardrails149 ]) {
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-guardrails153 assert.ok(!heavy(path), path);
154 }
155});
156
157test("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. */
165function 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
172test("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 address195
196test("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.