Skip to content

test(sdk): pin the invariants a reviewer cannot see - #36

Merged
cport1 merged 1 commit into
mainfrom
test/one-answer-invariants
Aug 22, 2026
Merged

test(sdk): pin the invariants a reviewer cannot see#36
cport1 merged 1 commit into
mainfrom
test/one-answer-invariants

Conversation

@cport1

@cport1 cport1 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Companion to WebDecoy/app's backend guardrails. Same reasoning: every finding in the 0.12/0.13 batch was two places answering one question, and the leftmost-X-Forwarded-For bug survived in two adapters after the same class was fixed elsewhere — because each call site read reasonably on its own and nothing connected them.

Four invariants

  • The client IP is resolved only in client-ip.ts
  • Decisions are built only through the Decision class, and never spread (a spread silently strips isDenied() / deniedBy() — the trap protect() originally fell into)
  • Every starvable rule has a NOT_RUN path, so "checked and fine" stays distinguishable from "never checked"
  • No node: import reaches a package that ships to Workers

Writing them found two things, which is the point

A false positive in my own rule. The first version flagged detection/detectors/headers.ts, which merely lists those header names for suspicious-shape detection. That is not IP resolution. The rule now matches actual access — headers['x-forwarded-for'] or .get(...) — rather than a mention.

A real hole in my own fix. It then flagged the Next.js adapter, correctly: the x-real-ip and x-vercel-forwarded-for fallbacks I left inline during the trusted-proxy work were header reads outside the resolver — exactly the drift the invariant exists to stop, reintroduced by the person who wrote the invariant.

They now live inside resolveClientIp, gated on a declared proxy: under trustProxy: false we read no forwarding header at all, because X-Real-IP is as forgeable as the rest. Four new tests cover the relocated behaviour, including that a real forwarding chain still wins over either.

Verification

386 core tests, 20/20 turbo tasks, all three edge entry points still compatible.

Every finding in the 0.12/0.13 batch was two places answering one
question, with the surface picking the flattering answer. The leftmost-XFF
bug survived in two adapters after the same class was fixed elsewhere,
because each call site read perfectly reasonably on its own and nothing
connected them.

Four source-reading invariants: the client IP is resolved only in
client-ip.ts, decisions are only ever built through the Decision class and
never spread (a spread silently strips isDenied() and deniedBy()), every
starvable rule has a NOT_RUN path, and no node: import reaches a package
that ships to Workers.

Writing them found two things, which is the point:

- The first version flagged detection/detectors/headers.ts, which merely
  LISTS those header names for suspicious-shape detection. The rule now
  matches actual access -- headers['x-forwarded-for'] or .get(...) -- not
  a mention.
- It then flagged the Next.js adapter, correctly, for my own code: the
  x-real-ip and x-vercel-forwarded-for fallbacks I left inline in the
  trusted-proxy fix were header reads outside the resolver, which is
  exactly the drift the invariant exists to stop. They now live in
  resolveClientIp, gated on a declared proxy -- under trustProxy: false we
  read no forwarding header at all, and X-Real-IP is as forgeable as the
  rest. Four tests cover the relocated behaviour.

386 core tests, 20/20 turbo tasks, three edge entry points still clean.
@cport1
cport1 merged commit ff61975 into main Aug 22, 2026
2 checks passed
@cport1
cport1 deleted the test/one-answer-invariants branch August 22, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant