agent402-tollbooth 0.1.0 → 0.1.2
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 +60 -0
- package/demo.js +58 -0
- package/edge.js +144 -0
- package/package.json +9 -2
- package/worker.js +47 -0
package/README.md
CHANGED
|
@@ -10,6 +10,31 @@ Stack Overflow) shipped pay-per-crawl as a closed, fiat, you-must-be-on-our-CDN
|
|
|
10
10
|
feature. This is the open, crypto-native, run-it-yourself version — built on the
|
|
11
11
|
same hardened 402 + proof-of-work machinery as [Agent402](https://github.com/MikeyPetrillo/Agent402).
|
|
12
12
|
|
|
13
|
+
## See it work (one command)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx agent402-tollbooth # then, in the repo: npm run --prefix tollbooth demo
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
agent402-tollbooth — live pay-per-crawl demo
|
|
21
|
+
|
|
22
|
+
① A human opens the page (normal browser)
|
|
23
|
+
→ HTTP 200 FREE "📄 The Future of Machine Payments — full article text…"
|
|
24
|
+
Humans are never charged.
|
|
25
|
+
|
|
26
|
+
② An AI crawler hits the same page (ClaudeBot)
|
|
27
|
+
→ HTTP 402 Payment Required
|
|
28
|
+
pay with USDC: $0.002 USDC on base → 0x…
|
|
29
|
+
…or free with proof-of-work: a 18-bit sha256 puzzle
|
|
30
|
+
|
|
31
|
+
③ The crawler has no wallet, so it spends CPU instead
|
|
32
|
+
solved in 0.32s (nonce=100208)
|
|
33
|
+
→ HTTP 200 OK (paid via pow) "📄 The Future of Machine Payments — full article text…"
|
|
34
|
+
|
|
35
|
+
✓ Pay-per-crawl, end to end — humans free, bots pay (USDC or compute).
|
|
36
|
+
```
|
|
37
|
+
|
|
13
38
|
## Install
|
|
14
39
|
|
|
15
40
|
```bash
|
|
@@ -61,6 +86,41 @@ TOLLBOOTH_PAYTO=0xYourWallet \
|
|
|
61
86
|
npx agent402-tollbooth # listens on :4021, proxies humans free, charges bots
|
|
62
87
|
```
|
|
63
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
|
+
|
|
64
124
|
## Accepting USDC (x402)
|
|
65
125
|
|
|
66
126
|
The proof-of-work rail works with **zero config**. To also settle real USDC,
|
package/demo.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Live, narrated pay-per-crawl demo. Spins up a "premium content" site behind
|
|
2
|
+
// the tollbooth, then shows a human getting it free and an AI crawler paying to
|
|
3
|
+
// get through (here: by solving a proof-of-work, since that needs no wallet).
|
|
4
|
+
//
|
|
5
|
+
// node demo.js (or: npm run demo)
|
|
6
|
+
import express from "express";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { createTollbooth } from "./index.js";
|
|
9
|
+
|
|
10
|
+
const c = { dim: "\x1b[2m", reset: "\x1b[0m", green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", bold: "\x1b[1m" };
|
|
11
|
+
const log = (s = "") => console.log(s);
|
|
12
|
+
const lz = (buf) => { let b = 0; for (const x of buf) { if (x === 0) { b += 8; continue; } b += Math.clz32(x) - 24; break; } return b; };
|
|
13
|
+
|
|
14
|
+
// A "premium content" site, fronted by the tollbooth.
|
|
15
|
+
const app = express();
|
|
16
|
+
app.use(createTollbooth({ price: "$0.002", payTo: "0xYourWalletHere000000000000000000000000", powDifficulty: 18 }));
|
|
17
|
+
app.use((_req, res) => res.send("📄 The Future of Machine Payments — full article text…"));
|
|
18
|
+
const server = app.listen(0);
|
|
19
|
+
const base = `http://localhost:${server.address().port}`;
|
|
20
|
+
const visit = (ua, extra = {}) => fetch(`${base}/article`, { headers: { "user-agent": ua, ...extra } });
|
|
21
|
+
|
|
22
|
+
const HUMAN = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
|
23
|
+
const CRAWLER = "Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)";
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
log(`${c.bold}agent402-tollbooth — live pay-per-crawl demo${c.reset}`);
|
|
27
|
+
log(`${c.dim}A premium-content site is now running behind the tollbooth.${c.reset}\n`);
|
|
28
|
+
|
|
29
|
+
// 1) A human browses — free.
|
|
30
|
+
log(`${c.cyan}① A human opens the page (normal browser)${c.reset}`);
|
|
31
|
+
let r = await visit(HUMAN);
|
|
32
|
+
log(` → HTTP ${r.status} ${c.green}FREE${c.reset} "${await r.text()}"`);
|
|
33
|
+
log(` ${c.dim}Humans are never charged.${c.reset}\n`);
|
|
34
|
+
|
|
35
|
+
// 2) An AI crawler hits the same page — 402.
|
|
36
|
+
log(`${c.cyan}② An AI crawler hits the same page (ClaudeBot)${c.reset}`);
|
|
37
|
+
r = await visit(CRAWLER);
|
|
38
|
+
const q = await r.json();
|
|
39
|
+
const a = q.accepts?.[0];
|
|
40
|
+
log(` → HTTP ${r.status} ${c.yellow}Payment Required${c.reset}`);
|
|
41
|
+
if (a) log(` ${c.dim}pay with USDC:${c.reset} ${a.maxAmountRequired} ${a.asset} on ${a.network} → ${a.payTo}`);
|
|
42
|
+
log(` ${c.dim}…or free with proof-of-work:${c.reset} a ${q.proofOfWork.difficulty}-bit sha256 puzzle\n`);
|
|
43
|
+
|
|
44
|
+
// 3) The crawler has no wallet, so it solves the proof-of-work.
|
|
45
|
+
log(`${c.cyan}③ The crawler has no wallet, so it spends CPU instead${c.reset}`);
|
|
46
|
+
const t0 = Date.now();
|
|
47
|
+
let nonce = 0;
|
|
48
|
+
while (lz(createHash("sha256").update(`${q.proofOfWork.challenge}:${nonce}`).digest()) < q.proofOfWork.difficulty) nonce++;
|
|
49
|
+
log(` ${c.dim}solved in ${((Date.now() - t0) / 1000).toFixed(2)}s (nonce=${nonce})${c.reset}`);
|
|
50
|
+
|
|
51
|
+
// 4) Retry with the solution — unlocked.
|
|
52
|
+
r = await visit(CRAWLER, { "x-pow-solution": `${q.proofOfWork.token}:${nonce}` });
|
|
53
|
+
log(` → HTTP ${r.status} ${c.green}OK${c.reset} (paid via ${r.headers.get("x-tollbooth-paid")}) "${await r.text()}"\n`);
|
|
54
|
+
|
|
55
|
+
log(`${c.bold}${c.green}✓ Pay-per-crawl, end to end${c.reset} — humans free, bots pay (USDC or compute). No Cloudflare, no Stripe, no signup.`);
|
|
56
|
+
} finally {
|
|
57
|
+
server.close();
|
|
58
|
+
}
|
package/edge.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
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 used = store || {
|
|
47
|
+
has: async (k) => { const e = mem.get(k); if (e && e < Date.now()) { mem.delete(k); return false; } return mem.has(k); },
|
|
48
|
+
add: async (k, exp) => { mem.set(k, exp); },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
async function challenge(resource) {
|
|
52
|
+
const rnd = new Uint8Array(16);
|
|
53
|
+
crypto.getRandomValues(rnd);
|
|
54
|
+
const chal = toHex(rnd);
|
|
55
|
+
const exp = Date.now() + ttlMs;
|
|
56
|
+
const payload = `${chal}.${exp}.${difficulty}.${resource}`;
|
|
57
|
+
const token = `${payload}.${await hmac(secret, payload)}`;
|
|
58
|
+
return {
|
|
59
|
+
algorithm: "sha256",
|
|
60
|
+
challenge: chal,
|
|
61
|
+
difficulty,
|
|
62
|
+
expires: exp,
|
|
63
|
+
token,
|
|
64
|
+
rule: `Find an integer nonce so sha256("${chal}:" + nonce) has >= ${difficulty} leading zero bits, then resend with header X-Pow-Solution: ${token}:<nonce>`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function verify(headerValue, resource) {
|
|
69
|
+
if (!headerValue || typeof headerValue !== "string") return { ok: false, reason: "missing solution" };
|
|
70
|
+
const cut = headerValue.lastIndexOf(":");
|
|
71
|
+
if (cut < 0) return { ok: false, reason: "malformed solution" };
|
|
72
|
+
const token = headerValue.slice(0, cut);
|
|
73
|
+
const nonce = headerValue.slice(cut + 1);
|
|
74
|
+
const parts = token.split(".");
|
|
75
|
+
if (parts.length !== 5) return { ok: false, reason: "malformed token" };
|
|
76
|
+
const [chal, expStr, diffStr, res, sig] = parts;
|
|
77
|
+
const expected = await hmac(secret, `${chal}.${expStr}.${diffStr}.${res}`);
|
|
78
|
+
if (!constEq(sig, expected)) return { ok: false, reason: "bad signature" };
|
|
79
|
+
if (res !== resource) return { ok: false, reason: "wrong resource" };
|
|
80
|
+
if (Date.now() > Number(expStr)) return { ok: false, reason: "expired" };
|
|
81
|
+
if (await used.has(token)) return { ok: false, reason: "already used" };
|
|
82
|
+
const h = await sha256(`${chal}:${nonce}`);
|
|
83
|
+
if (leadingZeroBits(h) < Number(diffStr)) return { ok: false, reason: "insufficient work" };
|
|
84
|
+
await used.add(token, Number(expStr));
|
|
85
|
+
return { ok: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { challenge, verify, difficulty };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create the edge gate. Returns `async (request) => Response | null`:
|
|
93
|
+
* a 402 `Response` when the client must pay, or `null` to allow the request
|
|
94
|
+
* through (proxy it, or `NextResponse.next()`).
|
|
95
|
+
*/
|
|
96
|
+
export function createEdgeTollbooth(config = {}) {
|
|
97
|
+
const {
|
|
98
|
+
price = "$0.001",
|
|
99
|
+
payTo = null,
|
|
100
|
+
network = "base",
|
|
101
|
+
asset = "USDC",
|
|
102
|
+
pow = true,
|
|
103
|
+
powDifficulty,
|
|
104
|
+
secret,
|
|
105
|
+
store,
|
|
106
|
+
botUserAgents = AI_BOTS,
|
|
107
|
+
charge,
|
|
108
|
+
free,
|
|
109
|
+
resourceBaseUrl = "",
|
|
110
|
+
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.",
|
|
111
|
+
} = config;
|
|
112
|
+
|
|
113
|
+
const isBot = makeBotMatcher(botUserAgents);
|
|
114
|
+
const powEngine = pow ? createEdgePow({ secret, difficulty: powDifficulty, store }) : null;
|
|
115
|
+
|
|
116
|
+
const shouldCharge = (request) => {
|
|
117
|
+
if (typeof free === "function" && free(request)) return false;
|
|
118
|
+
if (typeof charge === "function") return Boolean(charge(request));
|
|
119
|
+
return isBot(request.headers.get("user-agent") || "");
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return async function gate(request) {
|
|
123
|
+
if (!shouldCharge(request)) return null;
|
|
124
|
+
const u = new URL(request.url);
|
|
125
|
+
const resource = (resourceBaseUrl ? resourceBaseUrl.replace(/\/$/, "") : "") + u.pathname + u.search;
|
|
126
|
+
|
|
127
|
+
const sol = request.headers.get("x-pow-solution");
|
|
128
|
+
if (powEngine && sol) {
|
|
129
|
+
const r = await powEngine.verify(sol, resource);
|
|
130
|
+
if (r.ok) return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const body = {
|
|
134
|
+
error: "Payment Required",
|
|
135
|
+
message,
|
|
136
|
+
accepts: payTo ? [{ scheme: "exact", network, maxAmountRequired: String(price), asset, payTo, resource }] : [],
|
|
137
|
+
};
|
|
138
|
+
if (powEngine) body.proofOfWork = await powEngine.challenge(resource);
|
|
139
|
+
return new Response(JSON.stringify(body), {
|
|
140
|
+
status: 402,
|
|
141
|
+
headers: { "content-type": "application/json", "x-tollbooth": "pay-per-crawl" },
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-tollbooth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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,16 +12,20 @@
|
|
|
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",
|
|
20
|
+
"demo.js",
|
|
18
21
|
"README.md"
|
|
19
22
|
],
|
|
20
23
|
"engines": {
|
|
21
24
|
"node": ">=20"
|
|
22
25
|
},
|
|
23
26
|
"scripts": {
|
|
24
|
-
"test": "node test.js",
|
|
27
|
+
"test": "node test.js && node edge.test.js",
|
|
28
|
+
"demo": "node demo.js",
|
|
25
29
|
"example": "node example.js"
|
|
26
30
|
},
|
|
27
31
|
"dependencies": {
|
|
@@ -39,6 +43,9 @@
|
|
|
39
43
|
"usdc",
|
|
40
44
|
"reverse-proxy",
|
|
41
45
|
"express-middleware",
|
|
46
|
+
"cloudflare-workers",
|
|
47
|
+
"nextjs",
|
|
48
|
+
"edge",
|
|
42
49
|
"self-hosted",
|
|
43
50
|
"mcp",
|
|
44
51
|
"ai-agents"
|
package/worker.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
const gate = createEdgeTollbooth({
|
|
26
|
+
secret: env.TOLLBOOTH_SECRET,
|
|
27
|
+
price: env.TOLLBOOTH_PRICE || "$0.001",
|
|
28
|
+
payTo: env.TOLLBOOTH_PAYTO || null,
|
|
29
|
+
network: env.TOLLBOOTH_NETWORK || "base",
|
|
30
|
+
powDifficulty: env.TOLLBOOTH_POW_BITS ? Number(env.TOLLBOOTH_POW_BITS) : undefined,
|
|
31
|
+
store: env.TOLLBOOTH_KV ? kvStore(env.TOLLBOOTH_KV) : undefined,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const blocked = await gate(request);
|
|
35
|
+
if (blocked) return blocked;
|
|
36
|
+
|
|
37
|
+
// Allowed → proxy to the origin.
|
|
38
|
+
const upstream = env.TOLLBOOTH_UPSTREAM;
|
|
39
|
+
if (!upstream) return new Response("Tollbooth: set TOLLBOOTH_UPSTREAM to your origin", { status: 500 });
|
|
40
|
+
const target = new URL(request.url);
|
|
41
|
+
const origin = new URL(upstream);
|
|
42
|
+
target.protocol = origin.protocol;
|
|
43
|
+
target.hostname = origin.hostname;
|
|
44
|
+
target.port = origin.port;
|
|
45
|
+
return fetch(new Request(target.toString(), request));
|
|
46
|
+
},
|
|
47
|
+
};
|