Skip to content

fix(web): wire three unused rate-limit ids and guard against new ones#2040

Open
DPS0340 wants to merge 3 commits into
CapSoftware:mainfrom
DPS0340:fix/wire-rate-limit-ids
Open

fix(web): wire three unused rate-limit ids and guard against new ones#2040
DPS0340 wants to merge 3 commits into
CapSoftware:mainfrom
DPS0340:fix/wire-rate-limit-ids

Conversation

@DPS0340

@DPS0340 DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown

What

Wires three declared-but-never-called rate-limit ids to the unauthenticated endpoints they name, and adds a test so a new unwired id fails CI instead of being found by reading.

Closes #2039.

Why

RATE_LIMIT_IDS declares 13 ids, each with a doc comment naming the abuse it guards. Counting references outside the declaration file, 7 were never called, so those endpoints ran with no limit:

$ git ls-files -z '*.ts' '*.tsx' | xargs -0 grep -l RATE_LIMIT_IDS.GUEST_CHECKOUT | grep -v lib/rate-limit.ts
(no output)

This is quiet because there is already one silent-failure mode by design — the file's own comment notes that an id with no matching Vercel Firewall dashboard rule fails open. A declared-but-uncalled id is a second one, and it is invisible from the dashboard side too: someone can create rl_guest_checkout in the Firewall, see it configured, and reasonably assume the endpoint is protected while no code ever calls it.

What this PR wires

Three endpoints I read end to end and confirmed are unauthenticated with no existing limit:

  • api/settings/billing/guest-checkout — no auth of any kind. Reads priceId/quantity from the body and calls stripe().checkout.sessions.create(...). Limited before anything is read from the body.
  • api/analytics/track — anonymous by design (provideOptionalAuth). Each accepted call is a Tinybird ingest and can fan out into createAnonymousViewNotification / sendFirstViewEmail. Exactly the cost the comment describes.
  • api/tools/loom-download — unauthenticated, downloads a remote video and can run it through ffmpeg. Limited before any fetching starts.

All three follow the existing api/docs/ask pattern: isRateLimited(id, { headers: request.headers }), return 429. They inherit the helper's existing best-effort semantics, so a firewall outage or a self-hosted deploy without the Firewall behaves exactly as it does today.

What this PR deliberately does NOT wire

AUTH_OTP_VERIFY, AUTH_OTP_SEND, MESSENGER_MESSAGE and DESKTOP_LOGS. I could not confidently locate the single right call site for each, and guessing at an auth or messaging path is not something I want to do blind in a security change. They are listed in a KNOWN_UNWIRED map in the test with a reason each, so:

  • the suite passes today,
  • a new unwired id fails immediately rather than joining an already-failing count,
  • and removing an entry is the explicit act of wiring one.

Tell me which of the four you want next and I will follow up with the call sites.

Testing

New __tests__/unit/rate-limit-ids.test.ts, 4 tests:

  • every declared id is referenced somewhere outside lib/rate-limit.ts, except the documented KNOWN_UNWIRED entries
  • no stale allowance: an id in KNOWN_UNWIRED that is now referenced fails, so the list cannot linger and mask a future regression
  • no phantom allowance: an entry naming an id that no longer exists fails
  • rule ids are unique

The scan uses git ls-files for its file list, so it sees exactly the tracked sources and never walks node_modules or .next.

Verification. I ran the test's logic against the real repo rather than a copy, since the result depends on the actual file set:

files scanned: 960
unused (should be []): []
stale allowances (should be []): []
unknown allowances (should be []): []

Revert check. Reverting just the analytics/track wiring and re-running:

after reverting ANALYTICS_TRACK wiring, unused = ["ANALYTICS_TRACK"]

so the test genuinely detects an unwired id rather than passing by construction.

What I did not do. I have not exercised these three routes against a live deploy with the Firewall rules present — isRateLimited returns false outside production, so the added branch is inert in a local run. The three ids still need matching Rate Limit rules in the Vercel Firewall dashboard for the protection to take effect, per the helper's own documentation; this PR only makes the code path exist.

@DPS0340

DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Author

@tembo please review

let source: string;
try {
source = readFileSync(join(WEB_ROOT, file), "utf8");
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching all readFileSync errors here can mask real failures and make the scan silently incomplete. Maybe only ignore ENOENT (deleted-from-worktree case) and rethrow otherwise.

Suggested change
} catch {
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
continue; // deleted from the worktree but still in the index
}
throw error;
}

* Uses git's own file list so the search sees exactly the tracked sources and
* never walks node_modules or .next.
*/
function referencedKeys(): Set<string> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

referencedKeys() shells out to git + reads every TS/TSX file; since it’s called in multiple tests, memoizing the result would keep this suite from doing the full scan more than once.

@DPS0340

DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Author

Both taken, pushed in 7237d2a.

Swallowing every read error. You're right, and it's the worse of the two because of what this test is for: a silently shrinking scan makes the guard pass while checking less, which is the exact failure mode the test exists to prevent. Now only ENOENT continues (tracked-but-deleted, the case the comment described); anything else rethrows. Verified both branches:

missing file:
  ENOENT -> continue (as designed)
directory (EISDIR):
  EISDIR -> rethrown
  caught at caller: EISDIR

Memoizing. Done. Measured against the real repo:

first scan 41ms, memoized call 0ms

so it's three fewer full walks of the 960 tracked files across the suite.

Re-ran the assertions against the actual repo after both changes — the answer depends on the real tracked file set, so I check it there rather than in a fixture:

unused: []
stale : []
unknown: []

@tembo please re-review.

{ cwd: WEB_ROOT, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
)
.split("\0")
.filter((file) => file && file !== DECLARATION_FILE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scan includes __tests__/**, so a RATE_LIMIT_IDS.X referenced only from tests could make an unwired id look "wired". Excluding tests from tracked keeps this guard focused on runtime call sites.

Suggested change
.filter((file) => file && file !== DECLARATION_FILE);
.filter((file) => file && file !== DECLARATION_FILE && !file.startsWith("__tests__/"));

@DPS0340

DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Author

Right again, and this one was the most consequential of the three — pushed in 3487ea1.

It's worse than "a test could mask an unwired id": this file itself names every key, so once it lands, the scan would see all 13 referenced and the guard would be permanently vacuous. It only passed review because the assertions happen to be written as RATE_LIMIT_IDS.${key} template interpolation rather than literal RATE_LIMIT_IDS.FOO text, which is a very thin thing to be relying on.

I proved the hole rather than assuming it. Staged a probe test whose only content is a reference to DESKTOP_LOGS (an id that is not wired), then ran the scan both ways:

with a test-only reference to DESKTOP_LOGS (an UNWIRED id):
  scanning tests too   -> unused = []           (DESKTOP_LOGS now looks wired)
  excluding tests      -> unused = []
  stale-allowance check, scanning tests ->  ["DESKTOP_LOGS"]   (false alarm)
  stale-allowance check, excluding tests -> []

So the test-inclusive scan both hides a genuinely unwired id and fires a false "stale allowance" failure telling you to delete a KNOWN_UNWIRED entry that is still accurate. Probe removed afterwards.

Current state of the tracked-file set, tests excluded:

files scanned (tests excluded): 856   (was 960)
unused : []
stale  : []
unknown: []

For the record, no id is test-only in main today — AGENT_TOKEN_EXCHANGE and AGENT_AUTHORIZATION appear in both tests and runtime, so nothing changes about which ids currently pass. This is purely closing the future hole.

@tembo please re-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 of 13 RATE_LIMIT_IDS are declared but never called, so those endpoints have no rate limit

1 participant