@gagandeep023/expose-tunnel 0.2.0 → 0.3.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.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @gagandeep023/expose-tunnel
2
2
 
3
- A self-hosted tunnel to expose local servers to the internet via `*.tunnel.gagandeep023.com` subdomains. An alternative to ngrok and localtunnel that runs on your own infrastructure.
3
+ A self-hosted tunnel to expose local servers to the internet. An alternative to ngrok and localtunnel that runs on your own infrastructure.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@gagandeep023/expose-tunnel.svg)](https://www.npmjs.com/package/@gagandeep023/expose-tunnel)
6
6
  [![license](https://img.shields.io/npm/l/@gagandeep023/expose-tunnel.svg)](https://github.com/Gagandeep023/expose-tunnel/blob/main/LICENSE)
@@ -8,15 +8,15 @@ A self-hosted tunnel to expose local servers to the internet via `*.tunnel.gagan
8
8
  ## How It Works
9
9
 
10
10
  ```
11
- Your Machine EC2 Relay Server Internet
11
+ Your Machine Your Relay Server Internet
12
12
  +--------------+ WebSocket +------------------------+ HTTPS +----------+
13
- | localhost: | -------------> | tunnel.gagandeep023.com| <--------- | Browser |
14
- | 3000 | <------------- | *.tunnel.gagandeep023 | ---------> | requests |
13
+ | localhost: | -------------> | tunnel.yourdomain.com | <--------- | Browser |
14
+ | 3000 | <------------- | *.tunnel.yourdomain | ---------> | requests |
15
15
  +--------------+ +------------------------+ +----------+
16
16
  ```
17
17
 
18
- 1. The client connects to the relay server via WebSocket
19
- 2. The relay server assigns a public subdomain (e.g., `abc123.tunnel.gagandeep023.com`)
18
+ 1. The client connects to your relay server via WebSocket
19
+ 2. The relay server assigns a public subdomain (e.g., `abc123.tunnel.yourdomain.com`)
20
20
  3. When external traffic hits that subdomain, the relay server pipes it through the WebSocket to your machine
21
21
  4. Your client forwards the request to `localhost:<port>` and sends the response back
22
22
 
@@ -26,51 +26,105 @@ Your Machine EC2 Relay Server Internet
26
26
  npm install @gagandeep023/expose-tunnel
27
27
  ```
28
28
 
29
+ Or run directly with npx (no install needed):
30
+
31
+ ```bash
32
+ npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --api-key sk_your_key
33
+ ```
34
+
29
35
  ## Quick Start
30
36
 
31
- ### CLI
37
+ ### 1. Set environment variables (recommended)
32
38
 
33
39
  ```bash
34
- # Set your API key
40
+ export EXPOSE_TUNNEL_SERVER=wss://tunnel.yourdomain.com
35
41
  export EXPOSE_TUNNEL_API_KEY=sk_your_key_here
42
+ ```
43
+
44
+ ### 2. Expose a local port
45
+
46
+ ```bash
47
+ npx @gagandeep023/expose-tunnel --port 3000
48
+ ```
49
+
50
+ Output:
36
51
 
37
- # Expose port 3000
52
+ ```
53
+ [expose-tunnel] Tunnel established!
54
+ [expose-tunnel] Public URL: https://abc123.tunnel.yourdomain.com
55
+ [expose-tunnel] Forwarding to: http://localhost:3000
56
+ [expose-tunnel] Press Ctrl+C to close the tunnel.
57
+ ```
58
+
59
+ ## CLI Usage Examples
60
+
61
+ ### Basic: expose a port (env vars set)
62
+
63
+ ```bash
64
+ # Requires EXPOSE_TUNNEL_SERVER and EXPOSE_TUNNEL_API_KEY env vars
38
65
  npx @gagandeep023/expose-tunnel --port 3000
66
+ ```
67
+
68
+ ### With a custom subdomain
39
69
 
40
- # With a custom subdomain
70
+ ```bash
41
71
  npx @gagandeep023/expose-tunnel --port 3000 --subdomain myapp
72
+ # -> https://myapp.tunnel.yourdomain.com
73
+ ```
74
+
75
+ ### Without a subdomain (random assigned)
42
76
 
43
- # Output:
44
- # [expose-tunnel] Tunnel established!
45
- # [expose-tunnel] Public URL: https://myapp.tunnel.gagandeep023.com
46
- # [expose-tunnel] Forwarding to: http://localhost:3000
47
- # [expose-tunnel] Press Ctrl+C to close the tunnel.
77
+ ```bash
78
+ npx @gagandeep023/expose-tunnel --port 3000
79
+ # -> https://a1b2c3d4.tunnel.yourdomain.com
48
80
  ```
49
81
 
50
- ### Programmatic API
82
+ ### With --server flag (no env var needed)
51
83
 
52
- ```typescript
53
- import { exposeTunnel } from '@gagandeep023/expose-tunnel';
84
+ ```bash
85
+ npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com
86
+ ```
54
87
 
55
- const tunnel = await exposeTunnel({
56
- port: 3000,
57
- apiKey: 'sk_your_key_here',
58
- });
88
+ ### With --server and --subdomain
59
89
 
60
- console.log(`Public URL: ${tunnel.url}`);
61
- // https://abc123.tunnel.gagandeep023.com
90
+ ```bash
91
+ npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --subdomain myapp
92
+ ```
62
93
 
63
- // Listen for events
64
- tunnel.on('request', (method, path, status) => {
65
- console.log(`${method} ${path} -> ${status}`);
66
- });
94
+ ### With --api-key flag (no env var needed)
67
95
 
68
- tunnel.on('error', (err) => {
69
- console.error(err);
70
- });
96
+ ```bash
97
+ npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --api-key sk_your_key
98
+ ```
71
99
 
72
- // Close when done
73
- await tunnel.close();
100
+ ### All flags, no env vars
101
+
102
+ ```bash
103
+ npx @gagandeep023/expose-tunnel \
104
+ --port 3000 \
105
+ --server wss://tunnel.yourdomain.com \
106
+ --subdomain myapp \
107
+ --api-key sk_your_key_here
108
+ ```
109
+
110
+ ### Custom local host
111
+
112
+ ```bash
113
+ # Forward to a different local hostname (default: localhost)
114
+ npx @gagandeep023/expose-tunnel --port 3000 --local-host 0.0.0.0
115
+ ```
116
+
117
+ ### Expose different ports
118
+
119
+ ```bash
120
+ # Expose a React dev server
121
+ npx @gagandeep023/expose-tunnel --port 5173 --subdomain react-app
122
+
123
+ # Expose an Express backend
124
+ npx @gagandeep023/expose-tunnel --port 3001 --subdomain api
125
+
126
+ # Expose a database admin panel
127
+ npx @gagandeep023/expose-tunnel --port 8080 --subdomain admin
74
128
  ```
75
129
 
76
130
  ## CLI Reference
@@ -78,72 +132,164 @@ await tunnel.close();
78
132
  ```
79
133
  Usage: expose-tunnel [options]
80
134
 
81
- Expose local servers to the internet via gagandeep023.com subdomains
135
+ Expose local servers to the internet via your own relay server
82
136
 
83
137
  Options:
84
138
  -V, --version output version number
85
139
  -p, --port <number> Local port to expose (required)
86
140
  -s, --subdomain <name> Request a specific subdomain
87
- --server <url> Relay server URL (default: wss://tunnel.gagandeep023.com)
141
+ --server <url> Relay server WebSocket URL (or set EXPOSE_TUNNEL_SERVER env var)
88
142
  --api-key <key> API key (or set EXPOSE_TUNNEL_API_KEY env var)
89
143
  --local-host <host> Local hostname to proxy to (default: localhost)
90
144
  -h, --help display help for command
91
145
  ```
92
146
 
93
- ## API Reference
147
+ ## Programmatic API
94
148
 
95
- ### `exposeTunnel(options)`
149
+ ### Basic usage
96
150
 
97
- Creates a tunnel and returns a `TunnelInstance`.
151
+ ```typescript
152
+ import { exposeTunnel } from '@gagandeep023/expose-tunnel';
98
153
 
99
- **Parameters:**
154
+ const tunnel = await exposeTunnel({
155
+ port: 3000,
156
+ server: 'wss://tunnel.yourdomain.com',
157
+ apiKey: 'sk_your_key_here',
158
+ });
100
159
 
101
- | Parameter | Type | Required | Default | Description |
102
- |-------------|----------|----------|-------------------------------------|--------------------------------------|
103
- | `port` | `number` | Yes | | Local port to expose |
104
- | `subdomain` | `string` | No | Random 8-char | Requested subdomain |
105
- | `server` | `string` | No | `wss://tunnel.gagandeep023.com` | Relay server WebSocket URL |
106
- | `apiKey` | `string` | No | `EXPOSE_TUNNEL_API_KEY` env var | Authentication key |
107
- | `localHost` | `string` | No | `localhost` | Local hostname to proxy requests to |
160
+ console.log(`Public URL: ${tunnel.url}`);
108
161
 
109
- **Returns:** `Promise<TunnelInstance>`
162
+ // Close when done
163
+ await tunnel.close();
164
+ ```
110
165
 
111
- ### `TunnelInstance`
166
+ ### With subdomain
112
167
 
113
- | Property/Method | Type | Description |
114
- |-----------------|-------------------------|--------------------------------------------------|
115
- | `url` | `string` | Public HTTPS URL of the tunnel |
116
- | `subdomain` | `string` | Assigned subdomain |
117
- | `close()` | `() => Promise<void>` | Closes the tunnel connection |
118
- | `on(event, fn)` | EventEmitter | Listen for `request`, `error`, or `close` events |
168
+ ```typescript
169
+ const tunnel = await exposeTunnel({
170
+ port: 3000,
171
+ server: 'wss://tunnel.yourdomain.com',
172
+ apiKey: 'sk_your_key_here',
173
+ subdomain: 'myapp',
174
+ });
119
175
 
120
- ### `TunnelClient`
176
+ console.log(tunnel.url);
177
+ // -> https://myapp.tunnel.yourdomain.com
178
+ ```
121
179
 
122
- For more control, use the `TunnelClient` class directly:
180
+ ### Without subdomain (random)
181
+
182
+ ```typescript
183
+ const tunnel = await exposeTunnel({
184
+ port: 3000,
185
+ server: 'wss://tunnel.yourdomain.com',
186
+ apiKey: 'sk_your_key_here',
187
+ });
188
+
189
+ console.log(tunnel.url);
190
+ // -> https://a1b2c3d4.tunnel.yourdomain.com
191
+ ```
192
+
193
+ ### Using env vars (no server/apiKey in code)
194
+
195
+ ```typescript
196
+ // Set EXPOSE_TUNNEL_SERVER and EXPOSE_TUNNEL_API_KEY env vars first
197
+ const tunnel = await exposeTunnel({ port: 3000 });
198
+ ```
199
+
200
+ ### Event listeners
201
+
202
+ ```typescript
203
+ const tunnel = await exposeTunnel({
204
+ port: 3000,
205
+ server: 'wss://tunnel.yourdomain.com',
206
+ apiKey: 'sk_your_key_here',
207
+ });
208
+
209
+ // Log incoming requests
210
+ tunnel.on('request', (method, path, status) => {
211
+ console.log(`${method} ${path} -> ${status}`);
212
+ });
213
+
214
+ // Handle errors
215
+ tunnel.on('error', (err) => {
216
+ console.error(err);
217
+ });
218
+
219
+ // Handle tunnel close
220
+ tunnel.on('close', () => {
221
+ console.log('Tunnel closed');
222
+ });
223
+
224
+ await tunnel.close();
225
+ ```
226
+
227
+ ### TunnelClient class (advanced)
123
228
 
124
229
  ```typescript
125
230
  import { TunnelClient } from '@gagandeep023/expose-tunnel';
126
231
 
127
232
  const client = new TunnelClient({
128
233
  port: 3000,
234
+ server: 'wss://tunnel.yourdomain.com',
129
235
  apiKey: 'sk_your_key_here',
130
236
  subdomain: 'myapp',
131
237
  });
132
238
 
133
239
  const instance = await client.connect();
240
+ console.log(instance.url);
241
+
134
242
  // ... use the tunnel
243
+
135
244
  await client.close();
136
245
  ```
137
246
 
247
+ ## API Reference
248
+
249
+ ### `exposeTunnel(options)`
250
+
251
+ Creates a tunnel and returns a `TunnelInstance`.
252
+
253
+ **Parameters:**
254
+
255
+ | Parameter | Type | Required | Default | Description |
256
+ |-------------|----------|----------|-----------------------------------|--------------------------------------|
257
+ | `port` | `number` | Yes | | Local port to expose |
258
+ | `server` | `string` | Yes* | `EXPOSE_TUNNEL_SERVER` env var | Relay server WebSocket URL |
259
+ | `apiKey` | `string` | Yes* | `EXPOSE_TUNNEL_API_KEY` env var | Authentication key |
260
+ | `subdomain` | `string` | No | Random 8-char | Requested subdomain |
261
+ | `localHost` | `string` | No | `localhost` | Local hostname to proxy requests to |
262
+
263
+ *Can be provided via env var instead.
264
+
265
+ **Returns:** `Promise<TunnelInstance>`
266
+
267
+ ### `TunnelInstance`
268
+
269
+ | Property/Method | Type | Description |
270
+ |-----------------|-------------------------|--------------------------------------------------|
271
+ | `url` | `string` | Public HTTPS URL of the tunnel |
272
+ | `subdomain` | `string` | Assigned subdomain |
273
+ | `close()` | `() => Promise<void>` | Closes the tunnel connection |
274
+ | `on(event, fn)` | EventEmitter | Listen for `request`, `error`, or `close` events |
275
+
138
276
  ## Environment Variables
139
277
 
140
- | Variable | Description |
141
- |--------------------------|--------------------------------------------|
142
- | `EXPOSE_TUNNEL_API_KEY` | API key for authenticating with the relay |
278
+ | Variable | Description |
279
+ |--------------------------|--------------------------------------------------|
280
+ | `EXPOSE_TUNNEL_SERVER` | Relay server WebSocket URL (e.g., `wss://tunnel.yourdomain.com`) |
281
+ | `EXPOSE_TUNNEL_API_KEY` | API key for authenticating with the relay server |
143
282
 
144
283
  ## Self-Hosting the Relay Server
145
284
 
146
- The package includes a relay server you can deploy on your own infrastructure.
285
+ The package includes a relay server you can deploy on your own infrastructure. See [SELF-HOSTING-GUIDE.md](./SELF-HOSTING-GUIDE.md) for the full step-by-step deployment guide.
286
+
287
+ ### Quick overview
288
+
289
+ 1. Deploy on any VPS (EC2, DigitalOcean, etc.)
290
+ 2. Set up wildcard DNS (`*.tunnel.yourdomain.com` -> your server IP)
291
+ 3. Configure Nginx with wildcard SSL
292
+ 4. Start the relay server with PM2
147
293
 
148
294
  ### Requirements
149
295
 
@@ -152,9 +298,7 @@ The package includes a relay server you can deploy on your own infrastructure.
152
298
  - Nginx with wildcard SSL certificate
153
299
  - PM2 (recommended) for process management
154
300
 
155
- ### Setup
156
-
157
- 1. Install the package:
301
+ ### Quick Setup
158
302
 
159
303
  ```bash
160
304
  mkdir expose-tunnel-server && cd expose-tunnel-server
@@ -162,76 +306,39 @@ npm init -y
162
306
  npm install @gagandeep023/expose-tunnel
163
307
  ```
164
308
 
165
- 2. Create a startup script (`start.js`):
166
-
167
- ```javascript
168
- const fs = require('fs');
169
- const path = require('path');
170
-
171
- // Load .env
172
- const envFile = fs.readFileSync(path.join(__dirname, '.env'), 'utf8');
173
- envFile.split('\n').forEach(line => {
174
- const [key, ...val] = line.split('=');
175
- if (key && val.length) process.env[key.trim()] = val.join('=').trim();
176
- });
177
-
178
- require('@gagandeep023/expose-tunnel/dist/server/index.js');
179
- ```
180
-
181
- 3. Configure environment:
309
+ Create `.env`:
182
310
 
183
311
  ```bash
184
- # .env
185
312
  RELAY_PORT=4040
186
313
  API_KEYS=sk_your_generated_key
187
314
  TUNNEL_DOMAIN=tunnel.yourdomain.com
188
315
  ```
189
316
 
190
- 4. Generate an API key:
317
+ Generate an API key:
191
318
 
192
319
  ```bash
193
320
  node -e "console.log('sk_' + require('crypto').randomBytes(24).toString('hex'))"
194
321
  ```
195
322
 
196
- 5. Start the relay server:
197
-
198
- ```bash
199
- node start.js
200
-
201
- # Or with PM2:
202
- pm2 start start.js --name expose-tunnel-relay
203
- ```
204
-
205
- 6. Configure Nginx (wildcard reverse proxy):
323
+ Create `start.js`:
206
324
 
207
- ```nginx
208
- server {
209
- listen 443 ssl;
210
- server_name tunnel.yourdomain.com *.tunnel.yourdomain.com;
325
+ ```javascript
326
+ const fs = require('fs');
327
+ const path = require('path');
211
328
 
212
- ssl_certificate /etc/letsencrypt/live/tunnel.yourdomain.com/fullchain.pem;
213
- ssl_certificate_key /etc/letsencrypt/live/tunnel.yourdomain.com/privkey.pem;
329
+ const envFile = fs.readFileSync(path.join(__dirname, '.env'), 'utf8');
330
+ envFile.split('\n').forEach(line => {
331
+ const [key, ...val] = line.split('=');
332
+ if (key && val.length) process.env[key.trim()] = val.join('=').trim();
333
+ });
214
334
 
215
- location / {
216
- proxy_pass http://127.0.0.1:4040;
217
- proxy_http_version 1.1;
218
- proxy_set_header Upgrade $http_upgrade;
219
- proxy_set_header Connection "upgrade";
220
- proxy_set_header Host $host;
221
- proxy_set_header X-Real-IP $remote_addr;
222
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
223
- proxy_set_header X-Forwarded-Proto $scheme;
224
- proxy_read_timeout 86400s;
225
- proxy_send_timeout 86400s;
226
- }
227
- }
335
+ require('@gagandeep023/expose-tunnel/dist/server/index.js');
228
336
  ```
229
337
 
230
- 7. Set up wildcard SSL with Let's Encrypt:
338
+ Start it:
231
339
 
232
340
  ```bash
233
- sudo certbot certonly --manual --preferred-challenges dns \
234
- -d tunnel.yourdomain.com -d "*.tunnel.yourdomain.com"
341
+ pm2 start start.js --name expose-tunnel-relay
235
342
  ```
236
343
 
237
344
  ### Health Check
package/dist/cli.js CHANGED
@@ -60,7 +60,6 @@ var logger = {
60
60
  };
61
61
 
62
62
  // src/client/tunnel-client.ts
63
- var DEFAULT_SERVER = "wss://tunnel.gagandeep023.com";
64
63
  var MAX_RECONNECT_ATTEMPTS = 5;
65
64
  var RECONNECT_BASE_DELAY = 1e3;
66
65
  var TunnelClient = class extends import_node_events.EventEmitter {
@@ -73,11 +72,15 @@ var TunnelClient = class extends import_node_events.EventEmitter {
73
72
  subdomain = "";
74
73
  constructor(options) {
75
74
  super();
75
+ const server = options.server || process.env.EXPOSE_TUNNEL_SERVER;
76
+ if (!server) {
77
+ throw new Error("Relay server URL required. Pass server option or set EXPOSE_TUNNEL_SERVER env var.");
78
+ }
76
79
  this.options = {
77
80
  port: options.port,
78
81
  host: options.host || "localhost",
79
82
  subdomain: options.subdomain || "",
80
- server: options.server || DEFAULT_SERVER,
83
+ server,
81
84
  apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || "",
82
85
  localHost: options.localHost || "localhost"
83
86
  };
@@ -283,7 +286,12 @@ async function exposeTunnel(options) {
283
286
 
284
287
  // src/cli.ts
285
288
  var program = new import_commander.Command();
286
- program.name("expose-tunnel").description("Expose local servers to the internet via gagandeep023.com subdomains").version("0.1.0").requiredOption("-p, --port <number>", "Local port to expose").option("-s, --subdomain <name>", "Request a specific subdomain").option("--server <url>", "Relay server URL", "wss://tunnel.gagandeep023.com").option("--api-key <key>", "API key (or set EXPOSE_TUNNEL_API_KEY env var)").option("--local-host <host>", "Local hostname to proxy to", "localhost").action(async (opts) => {
289
+ program.name("expose-tunnel").description("Expose local servers to the internet via your own relay server").version("0.3.0").requiredOption("-p, --port <number>", "Local port to expose").option("-s, --subdomain <name>", "Request a specific subdomain").option("--server <url>", "Relay server WebSocket URL (or set EXPOSE_TUNNEL_SERVER env var)").option("--api-key <key>", "API key (or set EXPOSE_TUNNEL_API_KEY env var)").option("--local-host <host>", "Local hostname to proxy to", "localhost").action(async (opts) => {
290
+ const server = opts.server || process.env.EXPOSE_TUNNEL_SERVER;
291
+ if (!server) {
292
+ logger.error("Relay server URL required. Use --server <url> or set EXPOSE_TUNNEL_SERVER env var.");
293
+ process.exit(1);
294
+ }
287
295
  const apiKey = opts.apiKey || process.env.EXPOSE_TUNNEL_API_KEY;
288
296
  if (!apiKey) {
289
297
  logger.error("API key required. Use --api-key or set EXPOSE_TUNNEL_API_KEY env var.");
@@ -298,7 +306,7 @@ program.name("expose-tunnel").description("Expose local servers to the internet
298
306
  const tunnel = await exposeTunnel({
299
307
  port,
300
308
  subdomain: opts.subdomain,
301
- server: opts.server,
309
+ server,
302
310
  apiKey,
303
311
  localHost: opts.localHost
304
312
  });
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/client/tunnel-client.ts","../src/utils/logger.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { exposeTunnel } from './client/tunnel-client';\nimport { logger } from './utils/logger';\n\nconst program = new Command();\n\nprogram\n .name('expose-tunnel')\n .description('Expose local servers to the internet via gagandeep023.com subdomains')\n .version('0.1.0')\n .requiredOption('-p, --port <number>', 'Local port to expose')\n .option('-s, --subdomain <name>', 'Request a specific subdomain')\n .option('--server <url>', 'Relay server URL', 'wss://tunnel.gagandeep023.com')\n .option('--api-key <key>', 'API key (or set EXPOSE_TUNNEL_API_KEY env var)')\n .option('--local-host <host>', 'Local hostname to proxy to', 'localhost')\n .action(async (opts) => {\n const apiKey = opts.apiKey || process.env.EXPOSE_TUNNEL_API_KEY;\n if (!apiKey) {\n logger.error('API key required. Use --api-key or set EXPOSE_TUNNEL_API_KEY env var.');\n process.exit(1);\n }\n\n const port = parseInt(opts.port, 10);\n if (isNaN(port) || port < 1 || port > 65535) {\n logger.error('Invalid port number. Must be between 1 and 65535.');\n process.exit(1);\n }\n\n try {\n const tunnel = await exposeTunnel({\n port,\n subdomain: opts.subdomain,\n server: opts.server,\n apiKey,\n localHost: opts.localHost,\n });\n\n logger.success('Tunnel established!');\n logger.info(`Public URL: ${tunnel.url}`);\n logger.info(`Forwarding to: http://${opts.localHost}:${port}`);\n logger.info('Press Ctrl+C to close the tunnel.\\n');\n\n tunnel.on('error', (...args: unknown[]) => {\n const err = args[0] as Error;\n logger.error(err.message);\n });\n\n const shutdown = async (): Promise<void> => {\n logger.info('\\nClosing tunnel...');\n await tunnel.close();\n process.exit(0);\n };\n\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n } catch (err) {\n logger.error(`Failed to establish tunnel: ${(err as Error).message}`);\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import WebSocket from 'ws';\nimport http from 'node:http';\nimport { EventEmitter } from 'node:events';\nimport { TunnelOptions, TunnelInstance, TunnelRequest, TunnelResponse, WSMessage } from '../types';\nimport { logger } from '../utils/logger';\n\nconst DEFAULT_SERVER = 'wss://tunnel.gagandeep023.com';\nconst MAX_RECONNECT_ATTEMPTS = 5;\nconst RECONNECT_BASE_DELAY = 1000;\n\nexport class TunnelClient extends EventEmitter {\n private ws: WebSocket | null = null;\n private options: Required<TunnelOptions>;\n private reconnectAttempts = 0;\n private heartbeatInterval: NodeJS.Timeout | null = null;\n private closed = false;\n\n url = '';\n subdomain = '';\n\n constructor(options: TunnelOptions) {\n super();\n this.options = {\n port: options.port,\n host: options.host || 'localhost',\n subdomain: options.subdomain || '',\n server: options.server || DEFAULT_SERVER,\n apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || '',\n localHost: options.localHost || 'localhost',\n };\n }\n\n connect(): Promise<TunnelInstance> {\n return new Promise((resolve, reject) => {\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.options.subdomain) {\n headers['x-subdomain'] = this.options.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n const onError = (err: Error): void => {\n this.ws?.removeAllListeners();\n reject(new Error(`Failed to connect to relay server: ${err.message}`));\n };\n\n this.ws.once('error', onError);\n\n this.ws.once('open', () => {\n this.ws?.removeListener('error', onError);\n this.setupListeners(resolve);\n });\n });\n }\n\n async close(): Promise<void> {\n this.closed = true;\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.close();\n }\n this.ws = null;\n this.emit('close');\n }\n\n private setupListeners(resolve: (instance: TunnelInstance) => void): void {\n if (!this.ws) return;\n\n let assigned = false;\n\n this.ws.on('message', (data) => {\n try {\n const message = JSON.parse(data.toString()) as WSMessage;\n\n switch (message.type) {\n case 'tunnel-assigned':\n this.url = message.url;\n this.subdomain = message.subdomain;\n this.reconnectAttempts = 0;\n assigned = true;\n this.setupHeartbeat();\n resolve(this.createInstance());\n break;\n\n case 'tunnel-request':\n this.handleTunnelRequest(message.request);\n break;\n\n case 'ping':\n this.ws?.send(JSON.stringify({ type: 'pong' }));\n break;\n\n case 'tunnel-error':\n logger.error(`Server error: ${message.message}`);\n this.emit('error', new Error(message.message));\n break;\n }\n } catch {\n logger.error('Failed to parse server message');\n }\n });\n\n this.ws.on('close', () => {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n\n if (!this.closed && assigned) {\n logger.info('Connection lost. Attempting to reconnect...');\n this.reconnect();\n }\n });\n\n this.ws.on('error', (err) => {\n logger.error(`WebSocket error: ${err.message}`);\n this.emit('error', err);\n });\n }\n\n private async handleTunnelRequest(request: TunnelRequest): Promise<void> {\n try {\n const response = await this.proxyToLocal(request);\n this.sendResponse(response);\n this.emit('request', request.method, request.path, response.status);\n logger.request(request.method, request.path, response.status);\n } catch (err) {\n const errorResponse: TunnelResponse = {\n id: request.id,\n status: 502,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(\n JSON.stringify({ error: 'Failed to reach local server', details: (err as Error).message })\n ).toString('base64'),\n };\n this.sendResponse(errorResponse);\n this.emit('request', request.method, request.path, 502);\n logger.request(request.method, request.path, 502);\n }\n }\n\n private proxyToLocal(request: TunnelRequest): Promise<TunnelResponse> {\n return new Promise((resolve, reject) => {\n const url = `http://${this.options.localHost}:${this.options.port}${request.path}`;\n const parsedUrl = new URL(url);\n\n const headers: Record<string, string> = { ...request.headers };\n // Replace host header with local host\n headers['host'] = `${this.options.localHost}:${this.options.port}`;\n // Remove connection-specific headers\n delete headers['connection'];\n delete headers['upgrade'];\n\n const options: http.RequestOptions = {\n hostname: parsedUrl.hostname,\n port: parsedUrl.port,\n path: parsedUrl.pathname + parsedUrl.search,\n method: request.method,\n headers,\n };\n\n const proxyReq = http.request(options, (proxyRes) => {\n const chunks: Buffer[] = [];\n\n proxyRes.on('data', (chunk: Buffer) => {\n chunks.push(chunk);\n });\n\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks);\n const responseHeaders: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(proxyRes.headers)) {\n if (value) {\n responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;\n }\n }\n\n resolve({\n id: request.id,\n status: proxyRes.statusCode || 500,\n headers: responseHeaders,\n body: body.length > 0 ? body.toString('base64') : null,\n });\n });\n });\n\n proxyReq.on('error', reject);\n\n // Send request body\n if (request.body) {\n proxyReq.write(Buffer.from(request.body, 'base64'));\n }\n\n proxyReq.end();\n });\n }\n\n private sendResponse(response: TunnelResponse): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n const message: WSMessage = { type: 'tunnel-response', response };\n this.ws.send(JSON.stringify(message));\n }\n }\n\n private reconnect(): void {\n if (this.closed || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n logger.error(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`);\n this.emit('close');\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1);\n\n logger.info(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);\n\n setTimeout(() => {\n if (this.closed) return;\n\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.subdomain) {\n headers['x-subdomain'] = this.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n this.ws.once('open', () => {\n logger.success('Reconnected!');\n this.setupListeners(() => {});\n });\n\n this.ws.once('error', () => {\n this.reconnect();\n });\n }, delay);\n }\n\n private setupHeartbeat(): void {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n }\n // Client doesn't need to send its own heartbeat; it responds to server pings\n // But we keep a reference for cleanup\n }\n\n private createInstance(): TunnelInstance {\n return {\n url: this.url,\n subdomain: this.subdomain,\n close: () => this.close(),\n on: (event: string, handler: (...args: unknown[]) => void) => {\n this.on(event, handler);\n },\n };\n }\n}\n\nexport async function exposeTunnel(options: TunnelOptions): Promise<TunnelInstance> {\n const client = new TunnelClient(options);\n return client.connect();\n}\n","const PREFIX = '[expose-tunnel]';\n\nconst colors = {\n reset: '\\x1b[0m',\n cyan: '\\x1b[36m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n dim: '\\x1b[2m',\n};\n\nexport const logger = {\n info(msg: string): void {\n console.log(`${colors.cyan}${PREFIX}${colors.reset} ${msg}`);\n },\n\n error(msg: string): void {\n console.error(`${colors.red}${PREFIX}${colors.reset} ${msg}`);\n },\n\n success(msg: string): void {\n console.log(`${colors.green}${PREFIX}${colors.reset} ${msg}`);\n },\n\n request(method: string, path: string, status: number): void {\n const statusColor = status < 400 ? colors.green : status < 500 ? colors.yellow : colors.red;\n console.log(\n `${colors.dim}${PREFIX}${colors.reset} ${method} ${path} ${statusColor}${status}${colors.reset}`\n );\n },\n};\n"],"mappings":"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","names":["WebSocket","http"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/client/tunnel-client.ts","../src/utils/logger.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { exposeTunnel } from './client/tunnel-client';\nimport { logger } from './utils/logger';\n\nconst program = new Command();\n\nprogram\n .name('expose-tunnel')\n .description('Expose local servers to the internet via your own relay server')\n .version('0.3.0')\n .requiredOption('-p, --port <number>', 'Local port to expose')\n .option('-s, --subdomain <name>', 'Request a specific subdomain')\n .option('--server <url>', 'Relay server WebSocket URL (or set EXPOSE_TUNNEL_SERVER env var)')\n .option('--api-key <key>', 'API key (or set EXPOSE_TUNNEL_API_KEY env var)')\n .option('--local-host <host>', 'Local hostname to proxy to', 'localhost')\n .action(async (opts) => {\n const server = opts.server || process.env.EXPOSE_TUNNEL_SERVER;\n if (!server) {\n logger.error('Relay server URL required. Use --server <url> or set EXPOSE_TUNNEL_SERVER env var.');\n process.exit(1);\n }\n\n const apiKey = opts.apiKey || process.env.EXPOSE_TUNNEL_API_KEY;\n if (!apiKey) {\n logger.error('API key required. Use --api-key or set EXPOSE_TUNNEL_API_KEY env var.');\n process.exit(1);\n }\n\n const port = parseInt(opts.port, 10);\n if (isNaN(port) || port < 1 || port > 65535) {\n logger.error('Invalid port number. Must be between 1 and 65535.');\n process.exit(1);\n }\n\n try {\n const tunnel = await exposeTunnel({\n port,\n subdomain: opts.subdomain,\n server,\n apiKey,\n localHost: opts.localHost,\n });\n\n logger.success('Tunnel established!');\n logger.info(`Public URL: ${tunnel.url}`);\n logger.info(`Forwarding to: http://${opts.localHost}:${port}`);\n logger.info('Press Ctrl+C to close the tunnel.\\n');\n\n tunnel.on('error', (...args: unknown[]) => {\n const err = args[0] as Error;\n logger.error(err.message);\n });\n\n const shutdown = async (): Promise<void> => {\n logger.info('\\nClosing tunnel...');\n await tunnel.close();\n process.exit(0);\n };\n\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n } catch (err) {\n logger.error(`Failed to establish tunnel: ${(err as Error).message}`);\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import WebSocket from 'ws';\nimport http from 'node:http';\nimport { EventEmitter } from 'node:events';\nimport { TunnelOptions, TunnelInstance, TunnelRequest, TunnelResponse, WSMessage } from '../types';\nimport { logger } from '../utils/logger';\n\nconst MAX_RECONNECT_ATTEMPTS = 5;\nconst RECONNECT_BASE_DELAY = 1000;\n\nexport class TunnelClient extends EventEmitter {\n private ws: WebSocket | null = null;\n private options: Required<TunnelOptions>;\n private reconnectAttempts = 0;\n private heartbeatInterval: NodeJS.Timeout | null = null;\n private closed = false;\n\n url = '';\n subdomain = '';\n\n constructor(options: TunnelOptions) {\n super();\n const server = options.server || process.env.EXPOSE_TUNNEL_SERVER;\n if (!server) {\n throw new Error('Relay server URL required. Pass server option or set EXPOSE_TUNNEL_SERVER env var.');\n }\n this.options = {\n port: options.port,\n host: options.host || 'localhost',\n subdomain: options.subdomain || '',\n server,\n apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || '',\n localHost: options.localHost || 'localhost',\n };\n }\n\n connect(): Promise<TunnelInstance> {\n return new Promise((resolve, reject) => {\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.options.subdomain) {\n headers['x-subdomain'] = this.options.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n const onError = (err: Error): void => {\n this.ws?.removeAllListeners();\n reject(new Error(`Failed to connect to relay server: ${err.message}`));\n };\n\n this.ws.once('error', onError);\n\n this.ws.once('open', () => {\n this.ws?.removeListener('error', onError);\n this.setupListeners(resolve);\n });\n });\n }\n\n async close(): Promise<void> {\n this.closed = true;\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.close();\n }\n this.ws = null;\n this.emit('close');\n }\n\n private setupListeners(resolve: (instance: TunnelInstance) => void): void {\n if (!this.ws) return;\n\n let assigned = false;\n\n this.ws.on('message', (data) => {\n try {\n const message = JSON.parse(data.toString()) as WSMessage;\n\n switch (message.type) {\n case 'tunnel-assigned':\n this.url = message.url;\n this.subdomain = message.subdomain;\n this.reconnectAttempts = 0;\n assigned = true;\n this.setupHeartbeat();\n resolve(this.createInstance());\n break;\n\n case 'tunnel-request':\n this.handleTunnelRequest(message.request);\n break;\n\n case 'ping':\n this.ws?.send(JSON.stringify({ type: 'pong' }));\n break;\n\n case 'tunnel-error':\n logger.error(`Server error: ${message.message}`);\n this.emit('error', new Error(message.message));\n break;\n }\n } catch {\n logger.error('Failed to parse server message');\n }\n });\n\n this.ws.on('close', () => {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n\n if (!this.closed && assigned) {\n logger.info('Connection lost. Attempting to reconnect...');\n this.reconnect();\n }\n });\n\n this.ws.on('error', (err) => {\n logger.error(`WebSocket error: ${err.message}`);\n this.emit('error', err);\n });\n }\n\n private async handleTunnelRequest(request: TunnelRequest): Promise<void> {\n try {\n const response = await this.proxyToLocal(request);\n this.sendResponse(response);\n this.emit('request', request.method, request.path, response.status);\n logger.request(request.method, request.path, response.status);\n } catch (err) {\n const errorResponse: TunnelResponse = {\n id: request.id,\n status: 502,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(\n JSON.stringify({ error: 'Failed to reach local server', details: (err as Error).message })\n ).toString('base64'),\n };\n this.sendResponse(errorResponse);\n this.emit('request', request.method, request.path, 502);\n logger.request(request.method, request.path, 502);\n }\n }\n\n private proxyToLocal(request: TunnelRequest): Promise<TunnelResponse> {\n return new Promise((resolve, reject) => {\n const url = `http://${this.options.localHost}:${this.options.port}${request.path}`;\n const parsedUrl = new URL(url);\n\n const headers: Record<string, string> = { ...request.headers };\n // Replace host header with local host\n headers['host'] = `${this.options.localHost}:${this.options.port}`;\n // Remove connection-specific headers\n delete headers['connection'];\n delete headers['upgrade'];\n\n const options: http.RequestOptions = {\n hostname: parsedUrl.hostname,\n port: parsedUrl.port,\n path: parsedUrl.pathname + parsedUrl.search,\n method: request.method,\n headers,\n };\n\n const proxyReq = http.request(options, (proxyRes) => {\n const chunks: Buffer[] = [];\n\n proxyRes.on('data', (chunk: Buffer) => {\n chunks.push(chunk);\n });\n\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks);\n const responseHeaders: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(proxyRes.headers)) {\n if (value) {\n responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;\n }\n }\n\n resolve({\n id: request.id,\n status: proxyRes.statusCode || 500,\n headers: responseHeaders,\n body: body.length > 0 ? body.toString('base64') : null,\n });\n });\n });\n\n proxyReq.on('error', reject);\n\n // Send request body\n if (request.body) {\n proxyReq.write(Buffer.from(request.body, 'base64'));\n }\n\n proxyReq.end();\n });\n }\n\n private sendResponse(response: TunnelResponse): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n const message: WSMessage = { type: 'tunnel-response', response };\n this.ws.send(JSON.stringify(message));\n }\n }\n\n private reconnect(): void {\n if (this.closed || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n logger.error(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`);\n this.emit('close');\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1);\n\n logger.info(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);\n\n setTimeout(() => {\n if (this.closed) return;\n\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.subdomain) {\n headers['x-subdomain'] = this.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n this.ws.once('open', () => {\n logger.success('Reconnected!');\n this.setupListeners(() => {});\n });\n\n this.ws.once('error', () => {\n this.reconnect();\n });\n }, delay);\n }\n\n private setupHeartbeat(): void {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n }\n // Client doesn't need to send its own heartbeat; it responds to server pings\n // But we keep a reference for cleanup\n }\n\n private createInstance(): TunnelInstance {\n return {\n url: this.url,\n subdomain: this.subdomain,\n close: () => this.close(),\n on: (event: string, handler: (...args: unknown[]) => void) => {\n this.on(event, handler);\n },\n };\n }\n}\n\nexport async function exposeTunnel(options: TunnelOptions): Promise<TunnelInstance> {\n const client = new TunnelClient(options);\n return client.connect();\n}\n","const PREFIX = '[expose-tunnel]';\n\nconst colors = {\n reset: '\\x1b[0m',\n cyan: '\\x1b[36m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n dim: '\\x1b[2m',\n};\n\nexport const logger = {\n info(msg: string): void {\n console.log(`${colors.cyan}${PREFIX}${colors.reset} ${msg}`);\n },\n\n error(msg: string): void {\n console.error(`${colors.red}${PREFIX}${colors.reset} ${msg}`);\n },\n\n success(msg: string): void {\n console.log(`${colors.green}${PREFIX}${colors.reset} ${msg}`);\n },\n\n request(method: string, path: string, status: number): void {\n const statusColor = status < 400 ? colors.green : status < 500 ? colors.yellow : colors.red;\n console.log(\n `${colors.dim}${PREFIX}${colors.reset} ${method} ${path} ${statusColor}${status}${colors.reset}`\n );\n },\n};\n"],"mappings":"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","names":["WebSocket","http"]}
package/dist/index.js CHANGED
@@ -69,7 +69,6 @@ var logger = {
69
69
  };
70
70
 
71
71
  // src/client/tunnel-client.ts
72
- var DEFAULT_SERVER = "wss://tunnel.gagandeep023.com";
73
72
  var MAX_RECONNECT_ATTEMPTS = 5;
74
73
  var RECONNECT_BASE_DELAY = 1e3;
75
74
  var TunnelClient = class extends import_node_events.EventEmitter {
@@ -82,11 +81,15 @@ var TunnelClient = class extends import_node_events.EventEmitter {
82
81
  subdomain = "";
83
82
  constructor(options) {
84
83
  super();
84
+ const server = options.server || process.env.EXPOSE_TUNNEL_SERVER;
85
+ if (!server) {
86
+ throw new Error("Relay server URL required. Pass server option or set EXPOSE_TUNNEL_SERVER env var.");
87
+ }
85
88
  this.options = {
86
89
  port: options.port,
87
90
  host: options.host || "localhost",
88
91
  subdomain: options.subdomain || "",
89
- server: options.server || DEFAULT_SERVER,
92
+ server,
90
93
  apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || "",
91
94
  localHost: options.localHost || "localhost"
92
95
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client/tunnel-client.ts","../src/utils/logger.ts"],"sourcesContent":["export { exposeTunnel } from './client/tunnel-client';\nexport { TunnelClient } from './client/tunnel-client';\nexport type {\n TunnelOptions,\n TunnelInstance,\n TunnelRequest,\n TunnelResponse,\n} from './types';\n","import WebSocket from 'ws';\nimport http from 'node:http';\nimport { EventEmitter } from 'node:events';\nimport { TunnelOptions, TunnelInstance, TunnelRequest, TunnelResponse, WSMessage } from '../types';\nimport { logger } from '../utils/logger';\n\nconst DEFAULT_SERVER = 'wss://tunnel.gagandeep023.com';\nconst MAX_RECONNECT_ATTEMPTS = 5;\nconst RECONNECT_BASE_DELAY = 1000;\n\nexport class TunnelClient extends EventEmitter {\n private ws: WebSocket | null = null;\n private options: Required<TunnelOptions>;\n private reconnectAttempts = 0;\n private heartbeatInterval: NodeJS.Timeout | null = null;\n private closed = false;\n\n url = '';\n subdomain = '';\n\n constructor(options: TunnelOptions) {\n super();\n this.options = {\n port: options.port,\n host: options.host || 'localhost',\n subdomain: options.subdomain || '',\n server: options.server || DEFAULT_SERVER,\n apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || '',\n localHost: options.localHost || 'localhost',\n };\n }\n\n connect(): Promise<TunnelInstance> {\n return new Promise((resolve, reject) => {\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.options.subdomain) {\n headers['x-subdomain'] = this.options.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n const onError = (err: Error): void => {\n this.ws?.removeAllListeners();\n reject(new Error(`Failed to connect to relay server: ${err.message}`));\n };\n\n this.ws.once('error', onError);\n\n this.ws.once('open', () => {\n this.ws?.removeListener('error', onError);\n this.setupListeners(resolve);\n });\n });\n }\n\n async close(): Promise<void> {\n this.closed = true;\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.close();\n }\n this.ws = null;\n this.emit('close');\n }\n\n private setupListeners(resolve: (instance: TunnelInstance) => void): void {\n if (!this.ws) return;\n\n let assigned = false;\n\n this.ws.on('message', (data) => {\n try {\n const message = JSON.parse(data.toString()) as WSMessage;\n\n switch (message.type) {\n case 'tunnel-assigned':\n this.url = message.url;\n this.subdomain = message.subdomain;\n this.reconnectAttempts = 0;\n assigned = true;\n this.setupHeartbeat();\n resolve(this.createInstance());\n break;\n\n case 'tunnel-request':\n this.handleTunnelRequest(message.request);\n break;\n\n case 'ping':\n this.ws?.send(JSON.stringify({ type: 'pong' }));\n break;\n\n case 'tunnel-error':\n logger.error(`Server error: ${message.message}`);\n this.emit('error', new Error(message.message));\n break;\n }\n } catch {\n logger.error('Failed to parse server message');\n }\n });\n\n this.ws.on('close', () => {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n\n if (!this.closed && assigned) {\n logger.info('Connection lost. Attempting to reconnect...');\n this.reconnect();\n }\n });\n\n this.ws.on('error', (err) => {\n logger.error(`WebSocket error: ${err.message}`);\n this.emit('error', err);\n });\n }\n\n private async handleTunnelRequest(request: TunnelRequest): Promise<void> {\n try {\n const response = await this.proxyToLocal(request);\n this.sendResponse(response);\n this.emit('request', request.method, request.path, response.status);\n logger.request(request.method, request.path, response.status);\n } catch (err) {\n const errorResponse: TunnelResponse = {\n id: request.id,\n status: 502,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(\n JSON.stringify({ error: 'Failed to reach local server', details: (err as Error).message })\n ).toString('base64'),\n };\n this.sendResponse(errorResponse);\n this.emit('request', request.method, request.path, 502);\n logger.request(request.method, request.path, 502);\n }\n }\n\n private proxyToLocal(request: TunnelRequest): Promise<TunnelResponse> {\n return new Promise((resolve, reject) => {\n const url = `http://${this.options.localHost}:${this.options.port}${request.path}`;\n const parsedUrl = new URL(url);\n\n const headers: Record<string, string> = { ...request.headers };\n // Replace host header with local host\n headers['host'] = `${this.options.localHost}:${this.options.port}`;\n // Remove connection-specific headers\n delete headers['connection'];\n delete headers['upgrade'];\n\n const options: http.RequestOptions = {\n hostname: parsedUrl.hostname,\n port: parsedUrl.port,\n path: parsedUrl.pathname + parsedUrl.search,\n method: request.method,\n headers,\n };\n\n const proxyReq = http.request(options, (proxyRes) => {\n const chunks: Buffer[] = [];\n\n proxyRes.on('data', (chunk: Buffer) => {\n chunks.push(chunk);\n });\n\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks);\n const responseHeaders: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(proxyRes.headers)) {\n if (value) {\n responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;\n }\n }\n\n resolve({\n id: request.id,\n status: proxyRes.statusCode || 500,\n headers: responseHeaders,\n body: body.length > 0 ? body.toString('base64') : null,\n });\n });\n });\n\n proxyReq.on('error', reject);\n\n // Send request body\n if (request.body) {\n proxyReq.write(Buffer.from(request.body, 'base64'));\n }\n\n proxyReq.end();\n });\n }\n\n private sendResponse(response: TunnelResponse): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n const message: WSMessage = { type: 'tunnel-response', response };\n this.ws.send(JSON.stringify(message));\n }\n }\n\n private reconnect(): void {\n if (this.closed || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n logger.error(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`);\n this.emit('close');\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1);\n\n logger.info(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);\n\n setTimeout(() => {\n if (this.closed) return;\n\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.subdomain) {\n headers['x-subdomain'] = this.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n this.ws.once('open', () => {\n logger.success('Reconnected!');\n this.setupListeners(() => {});\n });\n\n this.ws.once('error', () => {\n this.reconnect();\n });\n }, delay);\n }\n\n private setupHeartbeat(): void {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n }\n // Client doesn't need to send its own heartbeat; it responds to server pings\n // But we keep a reference for cleanup\n }\n\n private createInstance(): TunnelInstance {\n return {\n url: this.url,\n subdomain: this.subdomain,\n close: () => this.close(),\n on: (event: string, handler: (...args: unknown[]) => void) => {\n this.on(event, handler);\n },\n };\n }\n}\n\nexport async function exposeTunnel(options: TunnelOptions): Promise<TunnelInstance> {\n const client = new TunnelClient(options);\n return client.connect();\n}\n","const PREFIX = '[expose-tunnel]';\n\nconst colors = {\n reset: '\\x1b[0m',\n cyan: '\\x1b[36m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n dim: '\\x1b[2m',\n};\n\nexport const logger = {\n info(msg: string): void {\n console.log(`${colors.cyan}${PREFIX}${colors.reset} ${msg}`);\n },\n\n error(msg: string): void {\n console.error(`${colors.red}${PREFIX}${colors.reset} ${msg}`);\n },\n\n success(msg: string): void {\n console.log(`${colors.green}${PREFIX}${colors.reset} ${msg}`);\n },\n\n request(method: string, path: string, status: number): void {\n const statusColor = status < 400 ? colors.green : status < 500 ? colors.yellow : colors.red;\n console.log(\n `${colors.dim}${PREFIX}${colors.reset} ${method} ${path} ${statusColor}${status}${colors.reset}`\n );\n },\n};\n"],"mappings":"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","names":["WebSocket","http"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client/tunnel-client.ts","../src/utils/logger.ts"],"sourcesContent":["export { exposeTunnel } from './client/tunnel-client';\nexport { TunnelClient } from './client/tunnel-client';\nexport type {\n TunnelOptions,\n TunnelInstance,\n TunnelRequest,\n TunnelResponse,\n} from './types';\n","import WebSocket from 'ws';\nimport http from 'node:http';\nimport { EventEmitter } from 'node:events';\nimport { TunnelOptions, TunnelInstance, TunnelRequest, TunnelResponse, WSMessage } from '../types';\nimport { logger } from '../utils/logger';\n\nconst MAX_RECONNECT_ATTEMPTS = 5;\nconst RECONNECT_BASE_DELAY = 1000;\n\nexport class TunnelClient extends EventEmitter {\n private ws: WebSocket | null = null;\n private options: Required<TunnelOptions>;\n private reconnectAttempts = 0;\n private heartbeatInterval: NodeJS.Timeout | null = null;\n private closed = false;\n\n url = '';\n subdomain = '';\n\n constructor(options: TunnelOptions) {\n super();\n const server = options.server || process.env.EXPOSE_TUNNEL_SERVER;\n if (!server) {\n throw new Error('Relay server URL required. Pass server option or set EXPOSE_TUNNEL_SERVER env var.');\n }\n this.options = {\n port: options.port,\n host: options.host || 'localhost',\n subdomain: options.subdomain || '',\n server,\n apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || '',\n localHost: options.localHost || 'localhost',\n };\n }\n\n connect(): Promise<TunnelInstance> {\n return new Promise((resolve, reject) => {\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.options.subdomain) {\n headers['x-subdomain'] = this.options.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n const onError = (err: Error): void => {\n this.ws?.removeAllListeners();\n reject(new Error(`Failed to connect to relay server: ${err.message}`));\n };\n\n this.ws.once('error', onError);\n\n this.ws.once('open', () => {\n this.ws?.removeListener('error', onError);\n this.setupListeners(resolve);\n });\n });\n }\n\n async close(): Promise<void> {\n this.closed = true;\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.close();\n }\n this.ws = null;\n this.emit('close');\n }\n\n private setupListeners(resolve: (instance: TunnelInstance) => void): void {\n if (!this.ws) return;\n\n let assigned = false;\n\n this.ws.on('message', (data) => {\n try {\n const message = JSON.parse(data.toString()) as WSMessage;\n\n switch (message.type) {\n case 'tunnel-assigned':\n this.url = message.url;\n this.subdomain = message.subdomain;\n this.reconnectAttempts = 0;\n assigned = true;\n this.setupHeartbeat();\n resolve(this.createInstance());\n break;\n\n case 'tunnel-request':\n this.handleTunnelRequest(message.request);\n break;\n\n case 'ping':\n this.ws?.send(JSON.stringify({ type: 'pong' }));\n break;\n\n case 'tunnel-error':\n logger.error(`Server error: ${message.message}`);\n this.emit('error', new Error(message.message));\n break;\n }\n } catch {\n logger.error('Failed to parse server message');\n }\n });\n\n this.ws.on('close', () => {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n\n if (!this.closed && assigned) {\n logger.info('Connection lost. Attempting to reconnect...');\n this.reconnect();\n }\n });\n\n this.ws.on('error', (err) => {\n logger.error(`WebSocket error: ${err.message}`);\n this.emit('error', err);\n });\n }\n\n private async handleTunnelRequest(request: TunnelRequest): Promise<void> {\n try {\n const response = await this.proxyToLocal(request);\n this.sendResponse(response);\n this.emit('request', request.method, request.path, response.status);\n logger.request(request.method, request.path, response.status);\n } catch (err) {\n const errorResponse: TunnelResponse = {\n id: request.id,\n status: 502,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(\n JSON.stringify({ error: 'Failed to reach local server', details: (err as Error).message })\n ).toString('base64'),\n };\n this.sendResponse(errorResponse);\n this.emit('request', request.method, request.path, 502);\n logger.request(request.method, request.path, 502);\n }\n }\n\n private proxyToLocal(request: TunnelRequest): Promise<TunnelResponse> {\n return new Promise((resolve, reject) => {\n const url = `http://${this.options.localHost}:${this.options.port}${request.path}`;\n const parsedUrl = new URL(url);\n\n const headers: Record<string, string> = { ...request.headers };\n // Replace host header with local host\n headers['host'] = `${this.options.localHost}:${this.options.port}`;\n // Remove connection-specific headers\n delete headers['connection'];\n delete headers['upgrade'];\n\n const options: http.RequestOptions = {\n hostname: parsedUrl.hostname,\n port: parsedUrl.port,\n path: parsedUrl.pathname + parsedUrl.search,\n method: request.method,\n headers,\n };\n\n const proxyReq = http.request(options, (proxyRes) => {\n const chunks: Buffer[] = [];\n\n proxyRes.on('data', (chunk: Buffer) => {\n chunks.push(chunk);\n });\n\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks);\n const responseHeaders: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(proxyRes.headers)) {\n if (value) {\n responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;\n }\n }\n\n resolve({\n id: request.id,\n status: proxyRes.statusCode || 500,\n headers: responseHeaders,\n body: body.length > 0 ? body.toString('base64') : null,\n });\n });\n });\n\n proxyReq.on('error', reject);\n\n // Send request body\n if (request.body) {\n proxyReq.write(Buffer.from(request.body, 'base64'));\n }\n\n proxyReq.end();\n });\n }\n\n private sendResponse(response: TunnelResponse): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n const message: WSMessage = { type: 'tunnel-response', response };\n this.ws.send(JSON.stringify(message));\n }\n }\n\n private reconnect(): void {\n if (this.closed || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n logger.error(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`);\n this.emit('close');\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1);\n\n logger.info(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);\n\n setTimeout(() => {\n if (this.closed) return;\n\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.subdomain) {\n headers['x-subdomain'] = this.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n this.ws.once('open', () => {\n logger.success('Reconnected!');\n this.setupListeners(() => {});\n });\n\n this.ws.once('error', () => {\n this.reconnect();\n });\n }, delay);\n }\n\n private setupHeartbeat(): void {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n }\n // Client doesn't need to send its own heartbeat; it responds to server pings\n // But we keep a reference for cleanup\n }\n\n private createInstance(): TunnelInstance {\n return {\n url: this.url,\n subdomain: this.subdomain,\n close: () => this.close(),\n on: (event: string, handler: (...args: unknown[]) => void) => {\n this.on(event, handler);\n },\n };\n }\n}\n\nexport async function exposeTunnel(options: TunnelOptions): Promise<TunnelInstance> {\n const client = new TunnelClient(options);\n return client.connect();\n}\n","const PREFIX = '[expose-tunnel]';\n\nconst colors = {\n reset: '\\x1b[0m',\n cyan: '\\x1b[36m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n dim: '\\x1b[2m',\n};\n\nexport const logger = {\n info(msg: string): void {\n console.log(`${colors.cyan}${PREFIX}${colors.reset} ${msg}`);\n },\n\n error(msg: string): void {\n console.error(`${colors.red}${PREFIX}${colors.reset} ${msg}`);\n },\n\n success(msg: string): void {\n console.log(`${colors.green}${PREFIX}${colors.reset} ${msg}`);\n },\n\n request(method: string, path: string, status: number): void {\n const statusColor = status < 400 ? colors.green : status < 500 ? colors.yellow : colors.red;\n console.log(\n `${colors.dim}${PREFIX}${colors.reset} ${method} ${path} ${statusColor}${status}${colors.reset}`\n );\n },\n};\n"],"mappings":"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","names":["WebSocket","http"]}
package/dist/index.mjs CHANGED
@@ -32,7 +32,6 @@ var logger = {
32
32
  };
33
33
 
34
34
  // src/client/tunnel-client.ts
35
- var DEFAULT_SERVER = "wss://tunnel.gagandeep023.com";
36
35
  var MAX_RECONNECT_ATTEMPTS = 5;
37
36
  var RECONNECT_BASE_DELAY = 1e3;
38
37
  var TunnelClient = class extends EventEmitter {
@@ -45,11 +44,15 @@ var TunnelClient = class extends EventEmitter {
45
44
  subdomain = "";
46
45
  constructor(options) {
47
46
  super();
47
+ const server = options.server || process.env.EXPOSE_TUNNEL_SERVER;
48
+ if (!server) {
49
+ throw new Error("Relay server URL required. Pass server option or set EXPOSE_TUNNEL_SERVER env var.");
50
+ }
48
51
  this.options = {
49
52
  port: options.port,
50
53
  host: options.host || "localhost",
51
54
  subdomain: options.subdomain || "",
52
- server: options.server || DEFAULT_SERVER,
55
+ server,
53
56
  apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || "",
54
57
  localHost: options.localHost || "localhost"
55
58
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client/tunnel-client.ts","../src/utils/logger.ts"],"sourcesContent":["import WebSocket from 'ws';\nimport http from 'node:http';\nimport { EventEmitter } from 'node:events';\nimport { TunnelOptions, TunnelInstance, TunnelRequest, TunnelResponse, WSMessage } from '../types';\nimport { logger } from '../utils/logger';\n\nconst DEFAULT_SERVER = 'wss://tunnel.gagandeep023.com';\nconst MAX_RECONNECT_ATTEMPTS = 5;\nconst RECONNECT_BASE_DELAY = 1000;\n\nexport class TunnelClient extends EventEmitter {\n private ws: WebSocket | null = null;\n private options: Required<TunnelOptions>;\n private reconnectAttempts = 0;\n private heartbeatInterval: NodeJS.Timeout | null = null;\n private closed = false;\n\n url = '';\n subdomain = '';\n\n constructor(options: TunnelOptions) {\n super();\n this.options = {\n port: options.port,\n host: options.host || 'localhost',\n subdomain: options.subdomain || '',\n server: options.server || DEFAULT_SERVER,\n apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || '',\n localHost: options.localHost || 'localhost',\n };\n }\n\n connect(): Promise<TunnelInstance> {\n return new Promise((resolve, reject) => {\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.options.subdomain) {\n headers['x-subdomain'] = this.options.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n const onError = (err: Error): void => {\n this.ws?.removeAllListeners();\n reject(new Error(`Failed to connect to relay server: ${err.message}`));\n };\n\n this.ws.once('error', onError);\n\n this.ws.once('open', () => {\n this.ws?.removeListener('error', onError);\n this.setupListeners(resolve);\n });\n });\n }\n\n async close(): Promise<void> {\n this.closed = true;\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.close();\n }\n this.ws = null;\n this.emit('close');\n }\n\n private setupListeners(resolve: (instance: TunnelInstance) => void): void {\n if (!this.ws) return;\n\n let assigned = false;\n\n this.ws.on('message', (data) => {\n try {\n const message = JSON.parse(data.toString()) as WSMessage;\n\n switch (message.type) {\n case 'tunnel-assigned':\n this.url = message.url;\n this.subdomain = message.subdomain;\n this.reconnectAttempts = 0;\n assigned = true;\n this.setupHeartbeat();\n resolve(this.createInstance());\n break;\n\n case 'tunnel-request':\n this.handleTunnelRequest(message.request);\n break;\n\n case 'ping':\n this.ws?.send(JSON.stringify({ type: 'pong' }));\n break;\n\n case 'tunnel-error':\n logger.error(`Server error: ${message.message}`);\n this.emit('error', new Error(message.message));\n break;\n }\n } catch {\n logger.error('Failed to parse server message');\n }\n });\n\n this.ws.on('close', () => {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n\n if (!this.closed && assigned) {\n logger.info('Connection lost. Attempting to reconnect...');\n this.reconnect();\n }\n });\n\n this.ws.on('error', (err) => {\n logger.error(`WebSocket error: ${err.message}`);\n this.emit('error', err);\n });\n }\n\n private async handleTunnelRequest(request: TunnelRequest): Promise<void> {\n try {\n const response = await this.proxyToLocal(request);\n this.sendResponse(response);\n this.emit('request', request.method, request.path, response.status);\n logger.request(request.method, request.path, response.status);\n } catch (err) {\n const errorResponse: TunnelResponse = {\n id: request.id,\n status: 502,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(\n JSON.stringify({ error: 'Failed to reach local server', details: (err as Error).message })\n ).toString('base64'),\n };\n this.sendResponse(errorResponse);\n this.emit('request', request.method, request.path, 502);\n logger.request(request.method, request.path, 502);\n }\n }\n\n private proxyToLocal(request: TunnelRequest): Promise<TunnelResponse> {\n return new Promise((resolve, reject) => {\n const url = `http://${this.options.localHost}:${this.options.port}${request.path}`;\n const parsedUrl = new URL(url);\n\n const headers: Record<string, string> = { ...request.headers };\n // Replace host header with local host\n headers['host'] = `${this.options.localHost}:${this.options.port}`;\n // Remove connection-specific headers\n delete headers['connection'];\n delete headers['upgrade'];\n\n const options: http.RequestOptions = {\n hostname: parsedUrl.hostname,\n port: parsedUrl.port,\n path: parsedUrl.pathname + parsedUrl.search,\n method: request.method,\n headers,\n };\n\n const proxyReq = http.request(options, (proxyRes) => {\n const chunks: Buffer[] = [];\n\n proxyRes.on('data', (chunk: Buffer) => {\n chunks.push(chunk);\n });\n\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks);\n const responseHeaders: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(proxyRes.headers)) {\n if (value) {\n responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;\n }\n }\n\n resolve({\n id: request.id,\n status: proxyRes.statusCode || 500,\n headers: responseHeaders,\n body: body.length > 0 ? body.toString('base64') : null,\n });\n });\n });\n\n proxyReq.on('error', reject);\n\n // Send request body\n if (request.body) {\n proxyReq.write(Buffer.from(request.body, 'base64'));\n }\n\n proxyReq.end();\n });\n }\n\n private sendResponse(response: TunnelResponse): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n const message: WSMessage = { type: 'tunnel-response', response };\n this.ws.send(JSON.stringify(message));\n }\n }\n\n private reconnect(): void {\n if (this.closed || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n logger.error(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`);\n this.emit('close');\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1);\n\n logger.info(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);\n\n setTimeout(() => {\n if (this.closed) return;\n\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.subdomain) {\n headers['x-subdomain'] = this.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n this.ws.once('open', () => {\n logger.success('Reconnected!');\n this.setupListeners(() => {});\n });\n\n this.ws.once('error', () => {\n this.reconnect();\n });\n }, delay);\n }\n\n private setupHeartbeat(): void {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n }\n // Client doesn't need to send its own heartbeat; it responds to server pings\n // But we keep a reference for cleanup\n }\n\n private createInstance(): TunnelInstance {\n return {\n url: this.url,\n subdomain: this.subdomain,\n close: () => this.close(),\n on: (event: string, handler: (...args: unknown[]) => void) => {\n this.on(event, handler);\n },\n };\n }\n}\n\nexport async function exposeTunnel(options: TunnelOptions): Promise<TunnelInstance> {\n const client = new TunnelClient(options);\n return client.connect();\n}\n","const PREFIX = '[expose-tunnel]';\n\nconst colors = {\n reset: '\\x1b[0m',\n cyan: '\\x1b[36m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n dim: '\\x1b[2m',\n};\n\nexport const logger = {\n info(msg: string): void {\n console.log(`${colors.cyan}${PREFIX}${colors.reset} ${msg}`);\n },\n\n error(msg: string): void {\n console.error(`${colors.red}${PREFIX}${colors.reset} ${msg}`);\n },\n\n success(msg: string): void {\n console.log(`${colors.green}${PREFIX}${colors.reset} ${msg}`);\n },\n\n request(method: string, path: string, status: number): void {\n const statusColor = status < 400 ? colors.green : status < 500 ? colors.yellow : colors.red;\n console.log(\n `${colors.dim}${PREFIX}${colors.reset} ${method} ${path} ${statusColor}${status}${colors.reset}`\n );\n },\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/client/tunnel-client.ts","../src/utils/logger.ts"],"sourcesContent":["import WebSocket from 'ws';\nimport http from 'node:http';\nimport { EventEmitter } from 'node:events';\nimport { TunnelOptions, TunnelInstance, TunnelRequest, TunnelResponse, WSMessage } from '../types';\nimport { logger } from '../utils/logger';\n\nconst MAX_RECONNECT_ATTEMPTS = 5;\nconst RECONNECT_BASE_DELAY = 1000;\n\nexport class TunnelClient extends EventEmitter {\n private ws: WebSocket | null = null;\n private options: Required<TunnelOptions>;\n private reconnectAttempts = 0;\n private heartbeatInterval: NodeJS.Timeout | null = null;\n private closed = false;\n\n url = '';\n subdomain = '';\n\n constructor(options: TunnelOptions) {\n super();\n const server = options.server || process.env.EXPOSE_TUNNEL_SERVER;\n if (!server) {\n throw new Error('Relay server URL required. Pass server option or set EXPOSE_TUNNEL_SERVER env var.');\n }\n this.options = {\n port: options.port,\n host: options.host || 'localhost',\n subdomain: options.subdomain || '',\n server,\n apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || '',\n localHost: options.localHost || 'localhost',\n };\n }\n\n connect(): Promise<TunnelInstance> {\n return new Promise((resolve, reject) => {\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.options.subdomain) {\n headers['x-subdomain'] = this.options.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n const onError = (err: Error): void => {\n this.ws?.removeAllListeners();\n reject(new Error(`Failed to connect to relay server: ${err.message}`));\n };\n\n this.ws.once('error', onError);\n\n this.ws.once('open', () => {\n this.ws?.removeListener('error', onError);\n this.setupListeners(resolve);\n });\n });\n }\n\n async close(): Promise<void> {\n this.closed = true;\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.close();\n }\n this.ws = null;\n this.emit('close');\n }\n\n private setupListeners(resolve: (instance: TunnelInstance) => void): void {\n if (!this.ws) return;\n\n let assigned = false;\n\n this.ws.on('message', (data) => {\n try {\n const message = JSON.parse(data.toString()) as WSMessage;\n\n switch (message.type) {\n case 'tunnel-assigned':\n this.url = message.url;\n this.subdomain = message.subdomain;\n this.reconnectAttempts = 0;\n assigned = true;\n this.setupHeartbeat();\n resolve(this.createInstance());\n break;\n\n case 'tunnel-request':\n this.handleTunnelRequest(message.request);\n break;\n\n case 'ping':\n this.ws?.send(JSON.stringify({ type: 'pong' }));\n break;\n\n case 'tunnel-error':\n logger.error(`Server error: ${message.message}`);\n this.emit('error', new Error(message.message));\n break;\n }\n } catch {\n logger.error('Failed to parse server message');\n }\n });\n\n this.ws.on('close', () => {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n\n if (!this.closed && assigned) {\n logger.info('Connection lost. Attempting to reconnect...');\n this.reconnect();\n }\n });\n\n this.ws.on('error', (err) => {\n logger.error(`WebSocket error: ${err.message}`);\n this.emit('error', err);\n });\n }\n\n private async handleTunnelRequest(request: TunnelRequest): Promise<void> {\n try {\n const response = await this.proxyToLocal(request);\n this.sendResponse(response);\n this.emit('request', request.method, request.path, response.status);\n logger.request(request.method, request.path, response.status);\n } catch (err) {\n const errorResponse: TunnelResponse = {\n id: request.id,\n status: 502,\n headers: { 'content-type': 'application/json' },\n body: Buffer.from(\n JSON.stringify({ error: 'Failed to reach local server', details: (err as Error).message })\n ).toString('base64'),\n };\n this.sendResponse(errorResponse);\n this.emit('request', request.method, request.path, 502);\n logger.request(request.method, request.path, 502);\n }\n }\n\n private proxyToLocal(request: TunnelRequest): Promise<TunnelResponse> {\n return new Promise((resolve, reject) => {\n const url = `http://${this.options.localHost}:${this.options.port}${request.path}`;\n const parsedUrl = new URL(url);\n\n const headers: Record<string, string> = { ...request.headers };\n // Replace host header with local host\n headers['host'] = `${this.options.localHost}:${this.options.port}`;\n // Remove connection-specific headers\n delete headers['connection'];\n delete headers['upgrade'];\n\n const options: http.RequestOptions = {\n hostname: parsedUrl.hostname,\n port: parsedUrl.port,\n path: parsedUrl.pathname + parsedUrl.search,\n method: request.method,\n headers,\n };\n\n const proxyReq = http.request(options, (proxyRes) => {\n const chunks: Buffer[] = [];\n\n proxyRes.on('data', (chunk: Buffer) => {\n chunks.push(chunk);\n });\n\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks);\n const responseHeaders: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(proxyRes.headers)) {\n if (value) {\n responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;\n }\n }\n\n resolve({\n id: request.id,\n status: proxyRes.statusCode || 500,\n headers: responseHeaders,\n body: body.length > 0 ? body.toString('base64') : null,\n });\n });\n });\n\n proxyReq.on('error', reject);\n\n // Send request body\n if (request.body) {\n proxyReq.write(Buffer.from(request.body, 'base64'));\n }\n\n proxyReq.end();\n });\n }\n\n private sendResponse(response: TunnelResponse): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n const message: WSMessage = { type: 'tunnel-response', response };\n this.ws.send(JSON.stringify(message));\n }\n }\n\n private reconnect(): void {\n if (this.closed || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {\n logger.error(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`);\n this.emit('close');\n }\n return;\n }\n\n this.reconnectAttempts++;\n const delay = RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1);\n\n logger.info(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);\n\n setTimeout(() => {\n if (this.closed) return;\n\n const wsUrl = `${this.options.server}/tunnel`;\n const headers: Record<string, string> = {\n 'x-api-key': this.options.apiKey,\n };\n\n if (this.subdomain) {\n headers['x-subdomain'] = this.subdomain;\n }\n\n this.ws = new WebSocket(wsUrl, { headers });\n\n this.ws.once('open', () => {\n logger.success('Reconnected!');\n this.setupListeners(() => {});\n });\n\n this.ws.once('error', () => {\n this.reconnect();\n });\n }, delay);\n }\n\n private setupHeartbeat(): void {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n }\n // Client doesn't need to send its own heartbeat; it responds to server pings\n // But we keep a reference for cleanup\n }\n\n private createInstance(): TunnelInstance {\n return {\n url: this.url,\n subdomain: this.subdomain,\n close: () => this.close(),\n on: (event: string, handler: (...args: unknown[]) => void) => {\n this.on(event, handler);\n },\n };\n }\n}\n\nexport async function exposeTunnel(options: TunnelOptions): Promise<TunnelInstance> {\n const client = new TunnelClient(options);\n return client.connect();\n}\n","const PREFIX = '[expose-tunnel]';\n\nconst colors = {\n reset: '\\x1b[0m',\n cyan: '\\x1b[36m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n dim: '\\x1b[2m',\n};\n\nexport const logger = {\n info(msg: string): void {\n console.log(`${colors.cyan}${PREFIX}${colors.reset} ${msg}`);\n },\n\n error(msg: string): void {\n console.error(`${colors.red}${PREFIX}${colors.reset} ${msg}`);\n },\n\n success(msg: string): void {\n console.log(`${colors.green}${PREFIX}${colors.reset} ${msg}`);\n },\n\n request(method: string, path: string, status: number): void {\n const statusColor = status < 400 ? colors.green : status < 500 ? colors.yellow : colors.red;\n console.log(\n `${colors.dim}${PREFIX}${colors.reset} ${method} ${path} ${statusColor}${status}${colors.reset}`\n );\n },\n};\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gagandeep023/expose-tunnel",
3
- "version": "0.2.0",
4
- "description": "Self-hosted tunnel to expose local servers to the internet via gagandeep023.com subdomains",
3
+ "version": "0.3.0",
4
+ "description": "Self-hosted tunnel to expose local servers to the internet. An ngrok/localtunnel alternative you run on your own infrastructure.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",