agent402-tollbooth 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,41 @@ TOLLBOOTH_PAYTO=0xYourWallet \
86
86
  npx agent402-tollbooth # listens on :4021, proxies humans free, charges bots
87
87
  ```
88
88
 
89
+ ## Run on the edge (Cloudflare Workers, Next.js, Deno, Bun)
90
+
91
+ The same gate is also built on the Web Crypto + Fetch APIs (`edge.js`), so it runs
92
+ anywhere — no Node required. The gate returns a `402 Response` when the client
93
+ must pay, or `null` to let it through.
94
+
95
+ **Cloudflare Workers** (poetic: the open pay-per-crawl, on the incumbent's own platform). `wrangler.toml`:
96
+
97
+ ```toml
98
+ name = "tollbooth"
99
+ main = "node_modules/agent402-tollbooth/worker.js"
100
+ [vars]
101
+ TOLLBOOTH_UPSTREAM = "https://your-origin.example.com"
102
+ TOLLBOOTH_PAYTO = "0xYourWallet" # optional: advertise a USDC x402 quote
103
+ # wrangler secret put TOLLBOOTH_SECRET
104
+ # optional single-use replay store: [[kv_namespaces]] binding = "TOLLBOOTH_KV"
105
+ ```
106
+
107
+ **Next.js middleware** (`middleware.ts`):
108
+
109
+ ```ts
110
+ import { createEdgeTollbooth } from "agent402-tollbooth/edge";
111
+ const gate = createEdgeTollbooth({ secret: process.env.TOLLBOOTH_SECRET! });
112
+
113
+ export async function middleware(req: Request) {
114
+ return (await gate(req)) ?? NextResponse.next();
115
+ }
116
+ ```
117
+
118
+ **Any Fetch-API runtime** (Deno, Bun, custom): `const gate = createEdgeTollbooth({ secret }); const blocked = await gate(request); return blocked ?? fetch(request);`
119
+
120
+ > On the edge, pass a stable `secret` (PoW tokens are HMAC-signed). For
121
+ > single-use replay protection across stateless invocations, supply a `store`
122
+ > (e.g. a Cloudflare KV wrapper — the Worker entry wires this for you).
123
+
89
124
  ## Accepting USDC (x402)
90
125
 
91
126
  The proof-of-work rail works with **zero config**. To also settle real USDC,
@@ -135,9 +170,25 @@ Amazonbot, …). Classic search indexers (Googlebot, Bingbot) are intentionally
135
170
  or take full control with a `charge(req)` predicate (e.g. charge anything
136
171
  without a browser-like `Accept` header, or charge everyone on `/api/*`).
137
172
 
173
+ ## Production checklist (read this)
174
+
175
+ - **Set a stable `TOLLBOOTH_SECRET`.** Required for any multi-process/clustered
176
+ Node deploy and for all edge deploys — without it, proof-of-work tokens use a
177
+ random per-process secret and are rejected across restarts/workers/isolates.
178
+ - **For serverless/edge, supply a durable replay `store`** (e.g. bind a Cloudflare
179
+ KV namespace as `TOLLBOOTH_KV`). The in-memory default is per-isolate, so a
180
+ solved token could be reused across isolates within its TTL. The Worker entry
181
+ warns when no KV is bound.
182
+ - **The reverse proxy pins the host** to your configured upstream (a client can't
183
+ redirect it elsewhere) and **strips client-forged trust/forwarding headers**
184
+ (`X-Tollbooth-Paid`, `X-Forwarded-Host`, etc.) before forwarding.
185
+ - **UA matching is the default, not a security boundary** — a bot can forge a
186
+ human UA to get the *same free access a human gets* (it gains nothing more). If
187
+ you need to charge everyone, pass `charge: () => true`.
188
+
138
189
  ## Notes
139
190
 
140
191
  - Proof-of-work tokens are HMAC-signed, expiry-checked, single-use, and bound to
141
- the exact resource — a solution for `/a` can't be replayed on `/b` or reused.
142
- - Set a stable `TOLLBOOTH_SECRET` in production so tokens survive restarts.
192
+ the exact resource (path + query, dots and all) — a solution for one URL can't
193
+ be replayed or reused on another.
143
194
  - MIT licensed. Part of [Agent402](https://github.com/MikeyPetrillo/Agent402).
package/edge.js ADDED
@@ -0,0 +1,156 @@
1
+ // Edge / runtime-agnostic tollbooth — the same pay-per-crawl gate, built on the
2
+ // Web Crypto + Fetch APIs so it runs anywhere: Cloudflare Workers, Next.js
3
+ // middleware (edge), Deno, Bun, and Node 20+. No node:* imports.
4
+ //
5
+ // const gate = createEdgeTollbooth({ secret: env.TOLLBOOTH_SECRET });
6
+ // const blocked = await gate(request); // Response(402) if must pay, else null
7
+ //
8
+ // See worker.js for a Cloudflare Worker entry, and the README for Next.js.
9
+ import { makeBotMatcher, AI_BOTS } from "./bots.js";
10
+
11
+ const te = new TextEncoder();
12
+
13
+ function b64url(buf) {
14
+ const bytes = new Uint8Array(buf);
15
+ let s = "";
16
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
17
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
18
+ }
19
+ const toHex = (bytes) => [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
20
+ function leadingZeroBits(bytes) {
21
+ let n = 0;
22
+ for (const b of bytes) { if (b === 0) { n += 8; continue; } n += Math.clz32(b) - 24; break; }
23
+ return n;
24
+ }
25
+ async function hmac(secret, payload) {
26
+ const key = await crypto.subtle.importKey("raw", te.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
27
+ return b64url(await crypto.subtle.sign("HMAC", key, te.encode(payload)));
28
+ }
29
+ const sha256 = async (str) => new Uint8Array(await crypto.subtle.digest("SHA-256", te.encode(str)));
30
+ function constEq(a, b) {
31
+ if (a.length !== b.length) return false;
32
+ let r = 0;
33
+ for (let i = 0; i < a.length; i++) r |= a.charCodeAt(i) ^ b.charCodeAt(i);
34
+ return r === 0;
35
+ }
36
+
37
+ /**
38
+ * Web Crypto proof-of-work. `secret` is required (edge runtimes can't keep a
39
+ * random per-process secret across invocations). `store` is an optional
40
+ * single-use replay backend ({ has(token), add(token, expMs) }, both async) —
41
+ * e.g. a Cloudflare KV wrapper; defaults to in-isolate memory.
42
+ */
43
+ export function createEdgePow({ secret, difficulty = 18, ttlMs = 5 * 60 * 1000, store } = {}) {
44
+ if (!secret) throw new Error("createEdgePow requires a stable `secret` string");
45
+ const mem = new Map();
46
+ const MAX = 50_000; // bound memory: edge runtimes have no timer to sweep, so prune on write
47
+ const used = store || {
48
+ has: async (k) => { const e = mem.get(k); if (e && e < Date.now()) { mem.delete(k); return false; } return mem.has(k); },
49
+ add: async (k, exp) => {
50
+ if (mem.size >= MAX) {
51
+ const now = Date.now();
52
+ for (const [kk, ee] of mem) if (ee < now) mem.delete(kk); // drop expired first
53
+ if (mem.size >= MAX) mem.delete(mem.keys().next().value); // hard cap: evict oldest
54
+ }
55
+ mem.set(k, exp);
56
+ },
57
+ };
58
+
59
+ async function challenge(resource) {
60
+ const rnd = new Uint8Array(16);
61
+ crypto.getRandomValues(rnd);
62
+ const chal = toHex(rnd);
63
+ const exp = Date.now() + ttlMs;
64
+ const payload = `${chal}.${exp}.${difficulty}.${resource}`;
65
+ const token = `${payload}.${await hmac(secret, payload)}`;
66
+ return {
67
+ algorithm: "sha256",
68
+ challenge: chal,
69
+ difficulty,
70
+ expires: exp,
71
+ token,
72
+ rule: `Find an integer nonce so sha256("${chal}:" + nonce) has >= ${difficulty} leading zero bits, then resend with header X-Pow-Solution: ${token}:<nonce>`,
73
+ };
74
+ }
75
+
76
+ async function verify(headerValue, resource) {
77
+ if (!headerValue || typeof headerValue !== "string") return { ok: false, reason: "missing solution" };
78
+ const cut = headerValue.lastIndexOf(":");
79
+ if (cut < 0) return { ok: false, reason: "malformed solution" };
80
+ const token = headerValue.slice(0, cut);
81
+ const nonce = headerValue.slice(cut + 1);
82
+ const parts = token.split(".");
83
+ if (parts.length < 5) return { ok: false, reason: "malformed token" };
84
+ const sig = parts.pop();
85
+ const [chal, expStr, diffStr] = parts;
86
+ const res = parts.slice(3).join("."); // resource may itself contain dots (e.g. /post.html?v=1.2)
87
+ const expected = await hmac(secret, parts.join("."));
88
+ if (!constEq(sig, expected)) return { ok: false, reason: "bad signature" };
89
+ if (res !== resource) return { ok: false, reason: "wrong resource" };
90
+ if (Date.now() > Number(expStr)) return { ok: false, reason: "expired" };
91
+ if (await used.has(token)) return { ok: false, reason: "already used" };
92
+ const h = await sha256(`${chal}:${nonce}`);
93
+ if (leadingZeroBits(h) < Number(diffStr)) return { ok: false, reason: "insufficient work" };
94
+ await used.add(token, Number(expStr));
95
+ return { ok: true };
96
+ }
97
+
98
+ return { challenge, verify, difficulty };
99
+ }
100
+
101
+ /**
102
+ * Create the edge gate. Returns `async (request) => Response | null`:
103
+ * a 402 `Response` when the client must pay, or `null` to allow the request
104
+ * through (proxy it, or `NextResponse.next()`).
105
+ */
106
+ export function createEdgeTollbooth(config = {}) {
107
+ const {
108
+ price = "$0.001",
109
+ payTo = null,
110
+ network = "base",
111
+ asset = "USDC",
112
+ pow = true,
113
+ powDifficulty,
114
+ secret,
115
+ store,
116
+ botUserAgents = AI_BOTS,
117
+ charge,
118
+ free,
119
+ resourceBaseUrl = "",
120
+ message = "This resource charges automated / AI clients per request. Humans browse free; bots pay in USDC via x402 or by solving a proof-of-work.",
121
+ } = config;
122
+
123
+ const isBot = makeBotMatcher(botUserAgents);
124
+ const powEngine = pow ? createEdgePow({ secret, difficulty: powDifficulty, store }) : null;
125
+
126
+ const shouldCharge = (request) => {
127
+ try {
128
+ if (typeof free === "function" && free(request)) return false;
129
+ if (typeof charge === "function") return Boolean(charge(request));
130
+ } catch { return true; /* fail closed: charge on predicate error */ }
131
+ return isBot(request.headers.get("user-agent") || "");
132
+ };
133
+
134
+ return async function gate(request) {
135
+ if (!shouldCharge(request)) return null;
136
+ const u = new URL(request.url);
137
+ const resource = (resourceBaseUrl ? resourceBaseUrl.replace(/\/$/, "") : "") + u.pathname + u.search;
138
+
139
+ const sol = request.headers.get("x-pow-solution");
140
+ if (powEngine && sol) {
141
+ const r = await powEngine.verify(sol, resource);
142
+ if (r.ok) return null;
143
+ }
144
+
145
+ const body = {
146
+ error: "Payment Required",
147
+ message,
148
+ accepts: payTo ? [{ scheme: "exact", network, maxAmountRequired: String(price), asset, payTo, resource }] : [],
149
+ };
150
+ if (powEngine) body.proofOfWork = await powEngine.challenge(resource);
151
+ return new Response(JSON.stringify(body), {
152
+ status: 402,
153
+ headers: { "content-type": "application/json", "x-tollbooth": "pay-per-crawl" },
154
+ });
155
+ };
156
+ }
package/index.js CHANGED
@@ -13,12 +13,22 @@
13
13
  // accept USDC, set `payTo` and supply `verifyX402` (wire it to the standard
14
14
  // x402 server middleware / your facilitator — see README).
15
15
  import { fileURLToPath } from "node:url";
16
+ import { Readable } from "node:stream";
16
17
  import { createPow } from "./pow.js";
17
18
  import { makeBotMatcher, AI_BOTS } from "./bots.js";
18
19
 
19
20
  export { AI_BOTS, makeBotMatcher } from "./bots.js";
20
21
  export { createPow, leadingZeroBits } from "./pow.js";
21
22
 
23
+ const VERIFY_TIMEOUT_MS = Number(process.env.TOLLBOOTH_VERIFY_TIMEOUT_MS) || 10_000;
24
+ // Headers a client must never be able to forge through the proxy: the gate's own
25
+ // trust signals and forwarding/hop-by-hop headers.
26
+ const STRIP_INBOUND = new Set([
27
+ "host", "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
28
+ "te", "trailer", "transfer-encoding", "upgrade", "content-length",
29
+ "x-tollbooth-paid", "x-tollbooth-error", "x-pow-error", "x-forwarded-host", "forwarded",
30
+ ]);
31
+
22
32
  /**
23
33
  * Create the tollbooth Express middleware.
24
34
  * @param {object} [config]
@@ -56,12 +66,20 @@ export function createTollbooth(config = {}) {
56
66
  const powEngine = pow ? createPow({ difficulty: powDifficulty, secret: powSecret }) : null;
57
67
 
58
68
  const shouldCharge = (req) => {
59
- if (typeof free === "function" && free(req)) return false;
60
- if (typeof charge === "function") return Boolean(charge(req));
69
+ try {
70
+ if (typeof free === "function" && free(req)) return false;
71
+ if (typeof charge === "function") return Boolean(charge(req));
72
+ } catch { return true; /* fail closed: charge on predicate error */ }
61
73
  return isBot(req.headers["user-agent"] || "");
62
74
  };
63
- const resourceOf = (req) =>
64
- (resourceBaseUrl ? resourceBaseUrl.replace(/\/$/, "") : "") + (req.originalUrl || req.url || "/");
75
+ const resourceOf = (req) => {
76
+ // Canonicalize to path+search (matches the edge impl) so the PoW binding is
77
+ // stable and not confusable via a raw/abnormal request target.
78
+ const raw = req.originalUrl || req.url || "/";
79
+ let pathAndSearch = raw;
80
+ try { const u = new URL(raw, "http://internal.invalid"); pathAndSearch = u.pathname + u.search; } catch { /* keep raw */ }
81
+ return (resourceBaseUrl ? resourceBaseUrl.replace(/\/$/, "") : "") + pathAndSearch;
82
+ };
65
83
 
66
84
  function tollbooth(req, res, next) {
67
85
  if (!shouldCharge(req)) return next();
@@ -92,7 +110,9 @@ export function createTollbooth(config = {}) {
92
110
  // we reuse the standard, audited x402 stack rather than reinvent it.
93
111
  const payHeader = req.headers["x-payment"] || req.headers["payment-signature"];
94
112
  if (payTo && typeof verifyX402 === "function" && payHeader) {
95
- return Promise.resolve(verifyX402(req, { price, network, asset, payTo, resource }))
113
+ // Bound verification time so a slow/hung verifier can't exhaust resources.
114
+ const timeout = new Promise((resolve) => setTimeout(() => resolve(false), VERIFY_TIMEOUT_MS));
115
+ return Promise.race([Promise.resolve(verifyX402(req, { price, network, asset, payTo, resource })), timeout])
96
116
  .then((ok) => {
97
117
  if (ok) { res.setHeader("X-Tollbooth-Paid", "x402"); return next(); }
98
118
  send402();
@@ -108,19 +128,25 @@ export function createTollbooth(config = {}) {
108
128
  return tollbooth;
109
129
  }
110
130
 
111
- /** Minimal reverse proxy: forward a request to `upstream`, streaming-safe-ish. */
112
- export function createProxy(upstream) {
131
+ /** Minimal reverse proxy: forward to `upstream` with the host PINNED (no SSRF via
132
+ * the request target), client trust/forwarding headers stripped, and the
133
+ * response STREAMED (no unbounded buffering). */
134
+ export function createProxy(upstream, { maxBody = 10 * 1024 * 1024 } = {}) {
135
+ const base = new URL(upstream);
113
136
  return async (req, res) => {
114
137
  try {
115
- const target = new URL(req.originalUrl || req.url, upstream);
138
+ // Take ONLY the path+query from the (possibly hostile) target; the
139
+ // authority is always the operator's upstream — protocol-relative or
140
+ // absolute-form targets cannot redirect us to another host.
141
+ const reqUrl = new URL(req.originalUrl || req.url || "/", base);
142
+ const target = new URL(reqUrl.pathname + reqUrl.search, base);
116
143
  const headers = {};
117
144
  for (const [k, v] of Object.entries(req.headers)) {
118
- const lk = k.toLowerCase();
119
- if (lk === "host" || lk === "connection" || lk === "content-length") continue;
120
- headers[k] = v;
145
+ if (!STRIP_INBOUND.has(k.toLowerCase())) headers[k] = v;
121
146
  }
147
+ headers["x-forwarded-for"] = req.socket?.remoteAddress || ""; // set by us, not the client
122
148
  const method = req.method;
123
- const body = method === "GET" || method === "HEAD" ? undefined : await readBody(req);
149
+ const body = method === "GET" || method === "HEAD" ? undefined : await readBody(req, maxBody);
124
150
  const up = await fetch(target, { method, headers, body, redirect: "manual" });
125
151
  res.status(up.status);
126
152
  up.headers.forEach((val, key) => {
@@ -129,9 +155,11 @@ export function createProxy(upstream) {
129
155
  if (["content-encoding", "content-length", "transfer-encoding", "connection"].includes(lk)) return;
130
156
  res.setHeader(key, val);
131
157
  });
132
- res.send(Buffer.from(await up.arrayBuffer()));
158
+ if (up.body) Readable.fromWeb(up.body).pipe(res);
159
+ else res.end();
133
160
  } catch (e) {
134
- res.status(502).json({ error: `tollbooth proxy failed: ${e.message}` });
161
+ if (!res.headersSent) res.status(502).json({ error: `tollbooth proxy failed: ${e.message}` });
162
+ else res.end();
135
163
  }
136
164
  };
137
165
  }
@@ -153,6 +181,9 @@ function readBody(req, cap = 10 * 1024 * 1024) {
153
181
  async function startCli() {
154
182
  const upstream = process.env.TOLLBOOTH_UPSTREAM;
155
183
  const port = Number(process.env.PORT) || 4021;
184
+ if (!process.env.TOLLBOOTH_SECRET) {
185
+ console.warn("⚠ TOLLBOOTH_SECRET not set — proof-of-work tokens use a random per-process secret: they won't survive a restart and will be rejected across multiple workers/instances. Set a stable TOLLBOOTH_SECRET in production.");
186
+ }
156
187
  const { default: express } = await import("express");
157
188
  const app = express();
158
189
  const gate = createTollbooth({ resourceBaseUrl: process.env.TOLLBOOTH_RESOURCE_BASE || upstream || "" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-tollbooth",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Open-source, self-hostable x402 pay-per-crawl tollbooth: charge AI bots per request (USDC via x402, or free proof-of-work) while humans browse free. Drop-in Express middleware or reverse proxy — no Cloudflare, no Stripe, no signup.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -12,6 +12,8 @@
12
12
  },
13
13
  "files": [
14
14
  "index.js",
15
+ "edge.js",
16
+ "worker.js",
15
17
  "bots.js",
16
18
  "pow.js",
17
19
  "example.js",
@@ -22,7 +24,7 @@
22
24
  "node": ">=20"
23
25
  },
24
26
  "scripts": {
25
- "test": "node test.js",
27
+ "test": "node test.js && node edge.test.js",
26
28
  "demo": "node demo.js",
27
29
  "example": "node example.js"
28
30
  },
@@ -41,6 +43,9 @@
41
43
  "usdc",
42
44
  "reverse-proxy",
43
45
  "express-middleware",
46
+ "cloudflare-workers",
47
+ "nextjs",
48
+ "edge",
44
49
  "self-hosted",
45
50
  "mcp",
46
51
  "ai-agents"
package/pow.js CHANGED
@@ -59,9 +59,11 @@ export function createPow({
59
59
  const token = headerValue.slice(0, cut);
60
60
  const nonce = headerValue.slice(cut + 1);
61
61
  const parts = token.split(".");
62
- if (parts.length !== 5) return { ok: false, reason: "malformed token" };
63
- const [chal, expStr, diffStr, res, sig] = parts;
64
- const expected = sign(`${chal}.${expStr}.${diffStr}.${res}`);
62
+ if (parts.length < 5) return { ok: false, reason: "malformed token" };
63
+ const sig = parts.pop();
64
+ const [chal, expStr, diffStr] = parts;
65
+ const res = parts.slice(3).join("."); // resource may itself contain dots (e.g. /post.html?v=1.2)
66
+ const expected = sign(parts.join("."));
65
67
  const a = Buffer.from(sig);
66
68
  const b = Buffer.from(expected);
67
69
  if (a.length !== b.length || !timingSafeEqual(a, b)) return { ok: false, reason: "bad signature" };
package/worker.js ADDED
@@ -0,0 +1,58 @@
1
+ // Cloudflare Worker entry for the tollbooth — open-source pay-per-crawl on the
2
+ // edge. Deploy in front of your origin: humans pass through, AI crawlers pay
3
+ // (USDC via x402, or free proof-of-work).
4
+ //
5
+ // wrangler.toml:
6
+ // name = "tollbooth"
7
+ // main = "node_modules/agent402-tollbooth/worker.js"
8
+ // [vars]
9
+ // TOLLBOOTH_UPSTREAM = "https://your-origin.example.com"
10
+ // TOLLBOOTH_PAYTO = "0xYourWallet" # optional (advertises USDC quote)
11
+ // # secret (required): wrangler secret put TOLLBOOTH_SECRET
12
+ // # optional single-use store: [[kv_namespaces]] binding = "TOLLBOOTH_KV"
13
+ import { createEdgeTollbooth } from "./edge.js";
14
+
15
+ const kvStore = (kv) => ({
16
+ has: async (k) => (await kv.get(k)) != null,
17
+ add: async (k, expMs) => kv.put(k, "1", { expiration: Math.ceil(expMs / 1000) }),
18
+ });
19
+
20
+ export default {
21
+ async fetch(request, env) {
22
+ if (!env.TOLLBOOTH_SECRET) {
23
+ return new Response("Tollbooth misconfigured: set TOLLBOOTH_SECRET (wrangler secret put TOLLBOOTH_SECRET)", { status: 500 });
24
+ }
25
+ if (!env.TOLLBOOTH_KV) {
26
+ // No durable store → replay protection is per-isolate only; a solved token
27
+ // can be reused across isolates within its TTL. Bind a KV namespace for prod.
28
+ console.warn("agent402-tollbooth: no TOLLBOOTH_KV bound — proof-of-work replay protection is per-isolate only. Bind a KV namespace for production.");
29
+ }
30
+ const gate = createEdgeTollbooth({
31
+ secret: env.TOLLBOOTH_SECRET,
32
+ price: env.TOLLBOOTH_PRICE || "$0.001",
33
+ payTo: env.TOLLBOOTH_PAYTO || null,
34
+ network: env.TOLLBOOTH_NETWORK || "base",
35
+ powDifficulty: env.TOLLBOOTH_POW_BITS ? Number(env.TOLLBOOTH_POW_BITS) : undefined,
36
+ store: env.TOLLBOOTH_KV ? kvStore(env.TOLLBOOTH_KV) : undefined,
37
+ });
38
+
39
+ const blocked = await gate(request);
40
+ if (blocked) return blocked;
41
+
42
+ // Allowed → proxy to the origin.
43
+ const upstream = env.TOLLBOOTH_UPSTREAM;
44
+ if (!upstream) return new Response("Tollbooth: set TOLLBOOTH_UPSTREAM to your origin", { status: 500 });
45
+ const target = new URL(request.url);
46
+ const origin = new URL(upstream);
47
+ target.protocol = origin.protocol;
48
+ target.hostname = origin.hostname;
49
+ target.port = origin.port;
50
+ // Strip client-forgeable trust/forwarding headers before forwarding to origin.
51
+ const headers = new Headers(request.headers);
52
+ for (const h of ["x-tollbooth-paid", "x-tollbooth-error", "x-pow-error", "x-forwarded-host", "forwarded"]) headers.delete(h);
53
+ headers.set("x-forwarded-for", request.headers.get("cf-connecting-ip") || "");
54
+ const init = { method: request.method, headers, redirect: "manual" };
55
+ if (request.method !== "GET" && request.method !== "HEAD") init.body = request.body;
56
+ return fetch(target.toString(), init);
57
+ },
58
+ };