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 | * The front door's rate limits (workers/app.ts), per request before it is | |
| 3 | * answered. The bindings and their limits are in `RATE_LIMITS` | |
| The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were. | 4 | * (packages/contracts/src/rate-limits.ts); CONTRIBUTING.md, "Rate limits", |
| 5 | * says how they are kept. | |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 6 | * |
| 7 | * - Git over HTTPS: without credentials, by address; with them, by a hash | |
| 8 | * of the credential, much higher. A clone is about three requests. | |
| 9 | * - Pages: every request that reaches the Worker counts against a ceiling | |
| 10 | * per address. Signed out, by address, with a tighter limit on what is | |
| 11 | * costly to answer (archives, run pages, logs, search). Signed in, by a | |
| 12 | * hash of the session cookie, higher: the session is not checked here, | |
| 13 | * which would cost a call to identity, and the ceiling per address keeps | |
| Merge main into Artifacts Phase 2 | 14 | * made-up cookies from getting round the signed-out limit. With an |
| 15 | * access token (`Authorization: Bearer`, app/lib/website-token.ts), by a | |
| 16 | * hash of the token, at the API's limit for a token; the address ceiling | |
| 17 | * applies as for cookies. | |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 18 | * |
| 19 | * Static assets never reach the Worker (the assets binding answers them), | |
| 20 | * and the few files it serves itself are left out here too. Every limit | |
| 21 | * fails open. | |
| 22 | */ | |
| 23 | import { | |
| 24 | type RateLimitBinding, | |
| 25 | checkLimit, | |
| 26 | clientAddress, | |
| 27 | secretKey, | |
| 28 | tooManyRequests, | |
| 29 | } from "@g1t/contracts/rate-limits"; | |
| 30 | ||
| 31 | export type FrontDoorLimits = { | |
| 32 | WEB_ANONYMOUS_LIMIT?: RateLimitBinding; | |
| 33 | WEB_HEAVY_LIMIT?: RateLimitBinding; | |
| 34 | WEB_SESSION_LIMIT?: RateLimitBinding; | |
| 35 | WEB_ADDRESS_LIMIT?: RateLimitBinding; | |
| Merge main into Artifacts Phase 2 | 36 | WEB_TOKEN_LIMIT?: RateLimitBinding; |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 37 | GIT_ANONYMOUS_LIMIT?: RateLimitBinding; |
| 38 | GIT_SIGNED_LIMIT?: RateLimitBinding; | |
| 39 | }; | |
| 40 | ||
| 41 | /** Files the Worker serves that are never limited: build output, fonts, and top-level files such as robots.txt. */ | |
| 42 | const UNLIMITED = /^\/(?:assets\/|fonts\/|favicon|[^/]+\.(?:ico|png|svg|txt|xml|webmanifest)$)/; | |
| 43 | ||
| 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. | 44 | /** |
| 45 | * What is costly to answer for a signed-out visitor, or must not be flooded: | |
| 46 | * a repository's archives, a run's page, logs and artifacts, search, and | |
| 47 | * the form that reports a vulnerability to a repository's maintainers | |
| 48 | * privately, which takes no account. | |
| 49 | */ | |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 50 | const HEAVY = |
| 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. | 51 | /^\/(?:search(?:\.data)?$|[^/]+\/[^/]+\/(?:archive\/|actions\/runs\/[^/]+(?:\.data|\/logs\.zip|\/artifacts\/[^/]+)?$|actions\/jobs\/[^/]+\/log|security\/report(?:\.data)?$))/; |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 52 | |
| 53 | export function unlimited(pathname: string): boolean { | |
| 54 | return UNLIMITED.test(pathname); | |
| 55 | } | |
| 56 | ||
| 57 | export function heavy(pathname: string): boolean { | |
| 58 | return HEAVY.test(pathname); | |
| 59 | } | |
| 60 | ||
| 61 | /** The session cookie's value, or null when signed out. */ | |
| 62 | export function sessionCookie(cookie: string | null): string | null { | |
| 63 | const match = /(?:^|;\s*)g1t_session=([^;]+)/.exec(cookie ?? ""); | |
| 64 | return match?.[1] ?? null; | |
| 65 | } | |
| 66 | ||
| 67 | const GIT_MESSAGE_ANONYMOUS = | |
| 68 | "Too many git requests from your network. Wait a minute and try again, or use credentials for a higher limit: https://docs.g1t.sh/reference/rate-limits/\n"; | |
| 69 | const GIT_MESSAGE_SIGNED = "Too many git requests with these credentials. Wait a minute and try again: https://docs.g1t.sh/reference/rate-limits/\n"; | |
| 70 | const PAGE_MESSAGE = "Too many requests from your network. Wait a minute and try again.\n"; | |
| Merge main into Artifacts Phase 2 | 71 | const TOKEN_PAGE_MESSAGE = "Too many requests with this access token. Wait a minute and try again: https://docs.g1t.sh/reference/rate-limits/\n"; |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 72 | |
| 73 | /** | |
| 74 | * The 429 for a git request past its limit, or null to go on. Git shows a | |
| 75 | * plain-text answer's body to the person running it. | |
| 76 | */ | |
| 77 | export async function gitLimited(env: FrontDoorLimits, request: Request): Promise<Response | null> { | |
| 78 | const credentials = request.headers.get("authorization"); | |
| 79 | if (credentials) { | |
| 80 | const verdict = await checkLimit(env.GIT_SIGNED_LIMIT, await secretKey("git", credentials)); | |
| 81 | return verdict === "limited" ? tooManyRequests(GIT_MESSAGE_SIGNED) : null; | |
| 82 | } | |
| 83 | const verdict = await checkLimit(env.GIT_ANONYMOUS_LIMIT, `ip:${clientAddress(request)}`); | |
| 84 | return verdict === "limited" ? tooManyRequests(GIT_MESSAGE_ANONYMOUS) : null; | |
| 85 | } | |
| 86 | ||
| Merge main into Artifacts Phase 2 | 87 | /** |
| 88 | * The token in a page request's `Authorization: Bearer` header, or null | |
| 89 | * (app/lib/website-token.ts). Not checked here either. | |
| 90 | */ | |
| 91 | export function websiteToken(authorization: string | null): string | null { | |
| 92 | return /^\s*bearer\s+(\S+)\s*$/i.exec(authorization ?? "")?.[1] ?? null; | |
| 93 | } | |
| 94 | ||
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 95 | /** The 429 for a page or data request past its limit, or null to go on. */ |
| 96 | export async function pageLimited(env: FrontDoorLimits, request: Request, pathname: string): Promise<Response | null> { | |
| 97 | if (unlimited(pathname)) return null; | |
| 98 | const address = `ip:${clientAddress(request)}`; | |
| Merge main into Artifacts Phase 2 | 99 | const token = websiteToken(request.headers.get("authorization")); |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 100 | const session = sessionCookie(request.headers.get("cookie")); |
| 101 | const checks: Promise<string>[] = [checkLimit(env.WEB_ADDRESS_LIMIT, address)]; | |
| Merge main into Artifacts Phase 2 | 102 | if (token) { |
| 103 | // A token on the website: per token, as the API counts it. | |
| 104 | checks.push(secretKey("token", token).then((key) => checkLimit(env.WEB_TOKEN_LIMIT, key))); | |
| 105 | } else if (session) { | |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 106 | checks.push(secretKey("session", session).then((key) => checkLimit(env.WEB_SESSION_LIMIT, key))); |
| 107 | } else { | |
| 108 | checks.push(checkLimit(env.WEB_ANONYMOUS_LIMIT, address)); | |
| 109 | if (heavy(pathname)) checks.push(checkLimit(env.WEB_HEAVY_LIMIT, address)); | |
| 110 | } | |
| 111 | const verdicts = await Promise.all(checks); | |
| 112 | if (!verdicts.includes("limited")) return null; | |
| Merge main into Artifacts Phase 2 | 113 | if (token && verdicts[1] === "limited") return tooManyRequests(TOKEN_PAGE_MESSAGE); |
| 114 | return tooManyRequests(token || session ? PAGE_MESSAGE : `${PAGE_MESSAGE.trimEnd()} Signed-in accounts have a higher limit.\n`); | |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 115 | } |
| 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 | 116 | |
| 117 | /** | |
| 118 | * The 429 for a repository file on the usercontent origin past its limit, | |
| 119 | * or null to go on. Nothing there is signed in (it never sees the session | |
| 120 | * cookie), so a file counts as a signed-out page from its address. Avatars | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 121 | * and custom emoji are answered from cache and are not limited. |
| 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 | 122 | */ |
| 123 | export async function usercontentLimited(env: FrontDoorLimits, request: Request, path: string): Promise<Response | null> { | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 124 | if (path.startsWith("/avatars/") || path.startsWith("/emoji/")) return null; |
| 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 | 125 | const verdict = await checkLimit(env.WEB_ANONYMOUS_LIMIT, `ip:${clientAddress(request)}`); |
| 126 | return verdict === "limited" ? tooManyRequests(PAGE_MESSAGE) : null; | |
| 127 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.