Skip to content

Commit f28b6b1

Browse files
therealalephclaude
andcommitted
docs(exit_node): ship wrapper.ts for VPS users (#912)
@BehroozAmoozad reported the docs reference a wrapper.ts file that doesn't exist in the repo — the README.md / README.fa.md "Your own VPS" row pointed at a wrapper.ts the user was supposed to write themselves. For non-JavaScript users, this was a hard blocker. Adds assets/exit_node/wrapper.ts that: - Imports the handler from exit_node.ts directly (so editing the PSK in exit_node.ts is the only setup step). - Auto-detects Deno, Bun, or Node 22+ at runtime and uses the matching HTTP server primitive (Deno.serve / Bun.serve / node:http). - Reads PORT, HOST, CERT_FILE, KEY_FILE from env. - Defaults to plain HTTP on port 8443 and recommends a reverse proxy (Caddy / nginx / Cloudflare Tunnel) for TLS termination — the path most VPS setups already have. - Optional standalone TLS via CERT_FILE + KEY_FILE for users without a reverse proxy. README.md + README.fa.md updated to link the file directly and adjust the deno run command to include the new --allow-env / --allow-read permissions the wrapper needs. No code changes — assets-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c774851 commit f28b6b1

3 files changed

Lines changed: 121 additions & 2 deletions

File tree

assets/exit_node/README.fa.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ config مثال کامل در
9494
|---|---|
9595
| **Deno Deploy** ([deno.com/deploy](https://deno.com/deploy)) | free tier برای personal use کافی است. با `deployctl deploy --prod exit_node.ts` یا GitHub Actions deploy کنید. همان web-standard API. |
9696
| **fly.io** | free tier با محدودیت. handler رو در یک server thin بسته‌بندی کنید (`Deno.serve(handler)` برای Deno یا یک Express wrapper برای Node) + Dockerfile اضافه کنید. IP دائم، region جغرافیایی قابل انتخاب. |
97-
| **VPS شخصی خودت** | `deno run --allow-net wrapper.ts` که `wrapper.ts` کارش `Deno.serve({ port: 8443 }, handler)` است. حداکثر کنترل، ~۳-۵ دلار در ماه. |
97+
| **VPS شخصی خودت** | از فایل آماده [`wrapper.ts`](wrapper.ts) استفاده کن: `deno run --allow-net --allow-env --allow-read wrapper.ts`. خودکار Deno / Bun / Node 22+ تشخیص می‌ده. حداکثر کنترل، ~۳-۵ دلار در ماه. |
9898
| **Cloudflare Workers** | **کمک نمی‌کنه.** CF Workers از IP space خود CF خروج می‌کنن، که CF anti-bot هنوز به‌عنوان worker-internal flag می‌کنه. |
9999

100100
برای اکثر کاربرانی که مسیر local رو اجرا می‌کنن، Deno Deploy

assets/exit_node/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ sign up for and trust:
9494
|---|---|
9595
| **Deno Deploy** ([deno.com/deploy](https://deno.com/deploy)) | Free tier covers personal use. Deploy via `deployctl deploy --prod exit_node.ts` or via GitHub Actions. Same web-standard API as the script expects. |
9696
| **fly.io** | Free tier with limits. Wrap the handler in a thin server (`Deno.serve(handler)` for Deno or an Express wrapper for Node) + add a Dockerfile. Persistent IPs, picks geographic region. |
97-
| **Your own VPS** | Run `deno run --allow-net wrapper.ts` where `wrapper.ts` does `Deno.serve({ port: 8443 }, handler)`. Most control, ~$3-5/mo. |
97+
| **Your own VPS** | Use the included [`wrapper.ts`](wrapper.ts): `deno run --allow-net --allow-env --allow-read wrapper.ts`. Auto-detects Deno / Bun / Node 22+. Most control, ~$3-5/mo. |
9898
| **Cloudflare Workers** | **Doesn't help.** CF Workers exit through CF's own IP space, which CF anti-bot still flags as worker-internal traffic. |
9999

100100
For most users running locally, Deno Deploy is the fastest setup. For

assets/exit_node/wrapper.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// VPS wrapper for exit_node.ts — used when you run the exit node on your
2+
// own server (any platform that can run Deno or Bun) instead of on a
3+
// platform that auto-invokes the default export (Deno Deploy, Val.town,
4+
// Cloudflare Workers, etc.).
5+
//
6+
// Pick ONE runtime + matching command:
7+
//
8+
// Deno (recommended, comes with HTTPS support out of the box):
9+
// deno run --allow-net --allow-env wrapper.ts
10+
//
11+
// Bun (also works, slightly faster cold start):
12+
// bun run wrapper.ts
13+
//
14+
// Node 22+ (no extra runtime; needs `--experimental-fetch` only on <22):
15+
// node wrapper.ts # if your Node has fetch + Bun's
16+
// # global Request/Response (22+)
17+
//
18+
// ENV VARS (all optional):
19+
// PORT — TCP port to bind. Default 8443.
20+
// HOST — bind address. Default 0.0.0.0 (all interfaces).
21+
// CERT_FILE — path to TLS cert PEM. Omit for plain HTTP (use a reverse
22+
// proxy like Caddy / nginx / Cloudflare Tunnel for TLS).
23+
// KEY_FILE — path to TLS key PEM (must be set together with CERT_FILE).
24+
//
25+
// Behind a reverse proxy (Caddy, nginx, Cloudflare Tunnel) you typically
26+
// run this on plain HTTP and let the proxy terminate TLS — that's the
27+
// simpler setup if you already have a domain. Set PORT=8443 and point
28+
// your reverse proxy at http://localhost:8443.
29+
//
30+
// Standalone TLS (rare but supported): set CERT_FILE + KEY_FILE to
31+
// matching PEM-encoded files and Deno will terminate TLS itself. Use
32+
// Let's Encrypt's certbot / acme.sh to fetch a real cert; self-signed
33+
// will not work (Apps Script's UrlFetchApp validates the chain).
34+
//
35+
// EDIT exit_node.ts FIRST: replace the placeholder PSK with a strong
36+
// secret. The wrapper imports the handler from exit_node.ts directly,
37+
// so changing the constant in exit_node.ts is all you need.
38+
39+
import handler from "./exit_node.ts";
40+
41+
// Deno (preferred)
42+
if (typeof (globalThis as any).Deno !== "undefined") {
43+
const Deno = (globalThis as any).Deno;
44+
const port = Number(Deno.env.get("PORT") ?? 8443);
45+
const hostname = Deno.env.get("HOST") ?? "0.0.0.0";
46+
const certFile = Deno.env.get("CERT_FILE");
47+
const keyFile = Deno.env.get("KEY_FILE");
48+
49+
if (certFile && keyFile) {
50+
Deno.serve(
51+
{
52+
port,
53+
hostname,
54+
cert: Deno.readTextFileSync(certFile),
55+
key: Deno.readTextFileSync(keyFile),
56+
},
57+
handler,
58+
);
59+
console.log(`exit_node listening on https://${hostname}:${port}`);
60+
} else {
61+
Deno.serve({ port, hostname }, handler);
62+
console.log(
63+
`exit_node listening on http://${hostname}:${port} ` +
64+
`(no TLS — terminate it with a reverse proxy like Caddy/nginx)`,
65+
);
66+
}
67+
}
68+
// Bun
69+
else if (typeof (globalThis as any).Bun !== "undefined") {
70+
const Bun = (globalThis as any).Bun;
71+
const port = Number(process.env.PORT ?? 8443);
72+
const hostname = process.env.HOST ?? "0.0.0.0";
73+
74+
Bun.serve({
75+
port,
76+
hostname,
77+
fetch: handler,
78+
tls: process.env.CERT_FILE && process.env.KEY_FILE
79+
? {
80+
cert: Bun.file(process.env.CERT_FILE),
81+
key: Bun.file(process.env.KEY_FILE),
82+
}
83+
: undefined,
84+
});
85+
console.log(`exit_node listening on ${hostname}:${port}`);
86+
}
87+
// Node 22+ — uses the built-in `node:http` module + globalThis.Request/Response
88+
else if (typeof (globalThis as any).process !== "undefined") {
89+
const { createServer } = await import("node:http");
90+
const port = Number(process.env.PORT ?? 8443);
91+
const hostname = process.env.HOST ?? "0.0.0.0";
92+
93+
createServer(async (req, res) => {
94+
// Build a web-standard Request from Node's IncomingMessage.
95+
const chunks: Uint8Array[] = [];
96+
for await (const c of req) chunks.push(c as Uint8Array);
97+
const body = chunks.length ? Buffer.concat(chunks) : undefined;
98+
99+
const url = `http://${req.headers.host ?? hostname}${req.url ?? "/"}`;
100+
const webReq = new Request(url, {
101+
method: req.method,
102+
headers: req.headers as Record<string, string>,
103+
body,
104+
});
105+
106+
const webRes = await handler(webReq);
107+
108+
res.statusCode = webRes.status;
109+
webRes.headers.forEach((v: string, k: string) => res.setHeader(k, v));
110+
const buf = new Uint8Array(await webRes.arrayBuffer());
111+
res.end(buf);
112+
}).listen(port, hostname, () => {
113+
console.log(`exit_node listening on http://${hostname}:${port}`);
114+
});
115+
} else {
116+
throw new Error(
117+
"No supported runtime detected. Run this file with Deno, Bun, or Node 22+.",
118+
);
119+
}

0 commit comments

Comments
 (0)