traforo 0.3.0 → 0.5.0

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 CHANGED
@@ -111,6 +111,29 @@ instructions to pass the password as a cookie:
111
111
  WebSocket upgrade requests without the correct cookie are rejected with
112
112
  close code 4013.
113
113
 
114
+ ## TRAFORO_URL ENVIRONMENT VARIABLE
115
+
116
+ When you run a command after `--`, traforo injects `TRAFORO_URL` into the
117
+ child process environment with the full public tunnel URL:
118
+
119
+ TRAFORO_URL=https://{tunnel-id}-tunnel.traforo.dev
120
+
121
+ Your app can read it directly:
122
+
123
+ const baseUrl = process.env.TRAFORO_URL
124
+
125
+ To remap it to a custom env var your app already uses, prefix the command:
126
+
127
+ traforo -p 3000 -- sh -c 'APP_URL=$TRAFORO_URL exec node server.js'
128
+ traforo -p 3000 -- sh -c 'NEXT_PUBLIC_URL=$TRAFORO_URL exec next dev'
129
+ traforo -p 3000 -- sh -c 'VITE_BASE_URL=$TRAFORO_URL exec vite'
130
+
131
+ Or set it in your .env / startup script and let traforo override only
132
+ `TRAFORO_URL`, reading it where needed:
133
+
134
+ // next.config.js
135
+ const baseUrl = process.env.APP_URL || process.env.TRAFORO_URL || 'http://localhost:3000'
136
+
114
137
  ## HOW IT WORKS
115
138
 
116
139
  1. Local client connects to Cloudflare Durable Object via WebSocket
package/dist/client.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Local tunnel client - runs on user's machine to expose a local server.
3
3
  */
4
+ import { HttpsProxyAgent } from 'https-proxy-agent';
5
+ import { SocksProxyAgent } from 'socks-proxy-agent';
4
6
  type TunnelClientOptions = {
5
7
  /** Local port to proxy to */
6
8
  localPort: number;
@@ -23,6 +25,9 @@ type TunnelClientOptions = {
23
25
  /** Password to protect the tunnel */
24
26
  password?: string;
25
27
  };
28
+ export declare function createWebSocketAgentFromEnv({ wsUrl, }: {
29
+ wsUrl: string;
30
+ }): HttpsProxyAgent<string> | SocksProxyAgent | undefined;
26
31
  export declare class TunnelClient {
27
32
  private options;
28
33
  private ws;
package/dist/client.js CHANGED
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Local tunnel client - runs on user's machine to expose a local server.
3
3
  */
4
+ import { HttpsProxyAgent } from 'https-proxy-agent';
5
+ import { getProxyForUrl } from 'proxy-from-env';
6
+ import { SocksProxyAgent } from 'socks-proxy-agent';
7
+ import { isAgent } from 'std-env';
4
8
  import WebSocket from 'ws';
5
9
  /**
6
10
  * Interval (ms) between keepalive pings sent to the DO.
@@ -9,6 +13,26 @@ import WebSocket from 'ws';
9
13
  * waking the DO from hibernation, so this is zero-cost.
10
14
  */
11
15
  const PING_INTERVAL_MS = 30_000;
16
+ export function createWebSocketAgentFromEnv({ wsUrl, }) {
17
+ const httpLikeUrl = (() => {
18
+ const parsedUrl = new URL(wsUrl);
19
+ if (parsedUrl.protocol === 'ws:') {
20
+ parsedUrl.protocol = 'http:';
21
+ }
22
+ if (parsedUrl.protocol === 'wss:') {
23
+ parsedUrl.protocol = 'https:';
24
+ }
25
+ return parsedUrl.toString();
26
+ })();
27
+ const proxyUrl = getProxyForUrl(wsUrl) || getProxyForUrl(httpLikeUrl);
28
+ if (!proxyUrl) {
29
+ return undefined;
30
+ }
31
+ if (proxyUrl.startsWith('socks')) {
32
+ return new SocksProxyAgent(proxyUrl);
33
+ }
34
+ return new HttpsProxyAgent(proxyUrl);
35
+ }
12
36
  export class TunnelClient {
13
37
  options;
14
38
  ws = null;
@@ -44,9 +68,18 @@ export class TunnelClient {
44
68
  }
45
69
  // console.log(`Connecting to ${wsUrl}...`)
46
70
  return new Promise((resolve, reject) => {
47
- this.ws = new WebSocket(wsUrl);
71
+ this.ws = new WebSocket(wsUrl, {
72
+ agent: createWebSocketAgentFromEnv({ wsUrl }),
73
+ });
48
74
  this.ws.on('open', () => {
49
- console.log(`Connected with Traforo!\n${this.url}`);
75
+ const { localHost, localPort, localHttps } = this.options;
76
+ const localProtocol = localHttps ? 'https' : 'http';
77
+ const localUrl = `${localProtocol}://${localHost}:${localPort}`;
78
+ let message = `Connected with Traforo!\n${this.url}`;
79
+ if (isAgent) {
80
+ message += `\n\nUse ${localUrl} directly for lower latency. The tunnel URL is for remote access. Show both URLs to the user.`;
81
+ }
82
+ console.log(message);
50
83
  this.startPingInterval();
51
84
  resolve();
52
85
  });
@@ -330,6 +330,9 @@ export async function runTunnel(options) {
330
330
  }
331
331
  }
332
332
  let child = null;
333
+ // Compute tunnel URL early so it can be injected into the child env
334
+ const baseDomain = options.baseDomain || 'traforo.dev';
335
+ const tunnelUrl = `https://${tunnelId}-tunnel.${baseDomain}`;
333
336
  // If command provided, spawn child process with PORT env
334
337
  if (options.command && options.command.length > 0) {
335
338
  const cmd = options.command[0];
@@ -347,6 +350,7 @@ export async function runTunnel(options) {
347
350
  env: {
348
351
  ...process.env,
349
352
  ...(port ? { PORT: String(port) } : {}),
353
+ TRAFORO_URL: tunnelUrl,
350
354
  // Disable clear/animations for common tools without lying about CI
351
355
  FORCE_COLOR: '1',
352
356
  VITE_CLS: 'false',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traforo",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "HTTP tunnel via Cloudflare Durable Objects and WebSockets. Edge caching and password protection.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,10 +28,23 @@
28
28
  "import": "./dist/run-tunnel.js"
29
29
  }
30
30
  },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.client.json && chmod +x dist/cli.js",
33
+ "prepublishOnly": "pnpm build",
34
+ "dev": "wrangler dev",
35
+ "deploy": "wrangler deploy --env preview",
36
+ "deploy:prod": "wrangler deploy # only run if user asks specifically!",
37
+ "typecheck": "tsc --noEmit",
38
+ "typecheck:client": "tsc --noEmit -p tsconfig.client.json",
39
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
40
+ "cli": "tsx src/cli.ts",
41
+ "test": "vitest --run"
42
+ },
31
43
  "devDependencies": {
32
44
  "@cloudflare/workers-types": "^4.20250712.0",
33
45
  "@types/http-cache-semantics": "^4.2.0",
34
46
  "@types/node": "^22.0.0",
47
+ "@types/proxy-from-env": "^1.0.4",
35
48
  "@types/ws": "^8.18.1",
36
49
  "tsx": "^4.20.5",
37
50
  "typescript": "^5.7.0",
@@ -43,18 +56,11 @@
43
56
  "dependencies": {
44
57
  "goke": "^6.8.0",
45
58
  "http-cache-semantics": "^4.2.0",
59
+ "https-proxy-agent": "^7.0.6",
60
+ "proxy-from-env": "^2.1.0",
61
+ "socks-proxy-agent": "^8.0.5",
62
+ "std-env": "^4.1.0",
46
63
  "string-dedent": "^3.0.2",
47
64
  "ws": "^8.19.0"
48
- },
49
- "scripts": {
50
- "build": "tsc -p tsconfig.client.json && chmod +x dist/cli.js",
51
- "dev": "wrangler dev",
52
- "deploy": "wrangler deploy --env preview",
53
- "deploy:prod": "wrangler deploy # only run if user asks specifically!",
54
- "typecheck": "tsc --noEmit",
55
- "typecheck:client": "tsc --noEmit -p tsconfig.client.json",
56
- "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
57
- "cli": "tsx src/cli.ts",
58
- "test": "vitest --run"
59
65
  }
60
- }
66
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Kimaki
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.