Skip to content

The cloud gate decided, then handed the verdict to something that overruled it - #364

Merged
ralyodio merged 1 commit into
masterfrom
cloud-gate-actually-charges
Sep 24, 2026
Merged

ralyodio merged 1 commit into
masterfrom
cloud-gate-actually-charges

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

#363 charged nobody. Caught by testing it against production rather than trusting the unit tests.

The bug

judgeCloudClient correctly identified a caller in a published cloud range, then called crawl-gateway's handle() — whose isPaidAgent defaults to the training-crawler list. That gateway re-decided the question we had just answered, saw an ordinary Chrome user agent (which is the entire point of this traffic — it is a real headless browser), returned null, and let the request through.

Verified against prod with the gate armed:

X-Forwarded-For: 3.0.0.1 (AWS ap-southeast-1) + Chrome UA  →  HTTP 200

Every unit test passed throughout, because the verdict was never the broken half. The delegation was, and nothing tested it.

The fix

Its own gateway with isPaidAgent: () => true — the same shape explorer-gateway already uses, for the same reason. By the time handle runs, who pays has been decided; asking a second, weaker rule to confirm it can only ever disagree.

Its own pass header (x-cloud-pass) and /cloud-pass sales page too, so a pass bought here is not confused with the crawl or explorer passes, which meter different things. That path is excluded from the gate — a sales page you can be refused for reading is a loop rather than an offer.

Testing

Tests now cover the integration, not only the verdict:

  • the gate actually answers 402
  • a non-cloud address passes through untouched
  • the user agent cannot overturn an address-based decision (curl, GPTBot and Chrome all charge alike)

11 in cloud-gate, 95 across the proxy + footprint suites, tsc --noEmit clean.

Note

CLOUD_CHARGE=pages is already set in prod, so this takes effect on deploy.

🤖 Generated with Claude Code

…rruled it

It charged nobody. `judgeCloudClient` correctly identified a caller in a
published cloud range and then called `crawl-gateway`'s `handle()`, whose
`isPaidAgent` defaults to the training-crawler list. That gateway re-decided
the question we had just answered, saw an ordinary Chrome user agent — which
is the entire point of this traffic, it is a real headless browser — returned
null, and let the request through.

Verified against production before and after: a request carrying an AWS
ap-southeast-1 address and a Chrome user agent was answered 200. Every unit
test passed throughout, because the verdict was never the broken half; the
delegation was, and nothing tested it.

So: its own gateway with `isPaidAgent: () => true`, the same shape
`explorer-gateway` already uses and for the same reason. By the time `handle`
runs, who pays has been decided; asking a second, weaker rule to confirm it
can only ever disagree. Its own pass header and `/cloud-pass` sales page too,
so a pass bought here is not confused with the crawl or explorer passes, which
meter different things — and that path is excluded from the gate, since a
sales page you can be refused for reading is a loop rather than an offer.

Tests now cover the integration rather than only the verdict: that the gate
answers 402, that a non-cloud address passes through untouched, and that the
user agent cannot overturn an address-based decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

331 finding(s)

HIGH/CRITICAL: 33 | MEDIUM: 40 | LOW: 258

Severity Rule Location
HIGH secret-private-key .env.example:236
HIGH secret-generic-api-key docs/API.md:430
HIGH secret-generic-api-key docs/API.md:585
HIGH secret-generic-credential docs/FIX_VERIFY_SIGNATURE.md:156
HIGH secret-generic-credential docs/integration-examples/nodejs-bot.md:225
HIGH secret-generic-api-key docs/sdk/getting-started.md:36
HIGH secret-generic-api-key docs/sdk/getting-started.md:318
HIGH secret-generic-credential packages/extension/scripts/make-screenshots.mjs:283
HIGH secret-generic-api-key packages/sdk/README.md:99
HIGH secret-generic-credential packages/sdk/README.md:122
HIGH secret-generic-credential packages/sdk/README.md:848
HIGH sh-remote-script-execution public/install.sh:167
HIGH sh-remote-script-execution public/install.sh:407
HIGH sh-remote-script-execution public/install.sh:412
HIGH sh-remote-script-execution public/install.sh:416
HIGH sh-remote-script-execution public/install.sh:761
HIGH sh-remote-script-execution public/install.sh:762
HIGH sh-remote-script-execution public/install.sh:802
HIGH sh-remote-script-execution public/install.sh:803
HIGH sh-remote-script-execution public/install.sh:804
HIGH secret-generic-credential scripts/setup-droplet.sh:609
HIGH secret-generic-api-key src/app/docs/sdk/page.tsx:135
HIGH secret-generic-api-key src/app/docs/sdk/page.tsx:214
HIGH secret-generic-credential src/app/docs/sdk/page.tsx:1001
HIGH secret-generic-credential src/app/docs/sdk/page.tsx:1022
HIGH secret-generic-api-key src/app/docs/sdk/page.tsx:1401
HIGH secret-generic-credential src/app/docs/sdk/page.tsx:1482
HIGH secret-generic-credential src/app/docs/sdk/page.tsx:1491
HIGH secret-generic-api-key src/app/docs/sdk/page.tsx:1533
HIGH secret-generic-credential src/components/docs/AuthenticationDocs.tsx:37
HIGH secret-generic-credential src/components/docs/OAuthDocs.tsx:262
HIGH secret-generic-credential supabase/config.toml:255
HIGH secret-generic-credential supabase/config.toml:287
MEDIUM manifest-install-lifecycle-script package.json:28
MEDIUM js-dynamic-code-execution packages/extension/scripts/make-screenshots.mjs:256
MEDIUM js-dynamic-code-execution packages/extension/scripts/make-screenshots.mjs:265
MEDIUM js-shell-exec-interpolation packages/sdk/bin/coinpay.js:49
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-issuer.test.js:23
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-reputation.test.js:23
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:22
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:33
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:48
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:63
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:78
MEDIUM js-shell-exec-interpolation packages/sdk/test/wallet-backup.test.js:82
MEDIUM js-shell-exec-interpolation packages/sdk/test/wallet.test.js:249
MEDIUM js-shell-exec-interpolation packages/sdk/test/wallet.test.js:280
MEDIUM insecure-temp-file public/install.sh:108
MEDIUM sh-unquoted-expansion-destructive public/install.sh:718
MEDIUM js-unescaped-html-sink public/payments.js:93

…and 281 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 0640309 into master Sep 24, 2026
9 checks passed
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.

1 participant