Skip to content
187 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
114test("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
123test("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
141test("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. */
149function 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
156test("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});
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 address179
180test("repository files on the usercontent origin count per address; avatars do not", async () => {
181 const env = { WEB_ANONYMOUS_LIMIT: binding(1) };
182 const raw = "/acme/rocket/raw/main/logo.png";
183 assert.equal(await usercontentLimited(env, request(raw), raw), null);
184 assert.equal((await usercontentLimited(env, request(raw), raw))?.status, 429);
185 assert.equal(await usercontentLimited(env, request("/avatars/" + "a".repeat(64)), "/avatars/" + "a".repeat(64)), null);
186 assert.deepEqual(env.WEB_ANONYMOUS_LIMIT.keys, ["ip:203.0.113.9", "ip:203.0.113.9"]);
187});

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