@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 +221 -114
- package/dist/cli.js +12 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
[](https://www.npmjs.com/package/@gagandeep023/expose-tunnel)
|
|
6
6
|
[](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
|
|
11
|
+
Your Machine Your Relay Server Internet
|
|
12
12
|
+--------------+ WebSocket +------------------------+ HTTPS +----------+
|
|
13
|
-
| localhost: | -------------> | tunnel.
|
|
14
|
-
| 3000 | <------------- | *.tunnel.
|
|
13
|
+
| localhost: | -------------> | tunnel.yourdomain.com | <--------- | Browser |
|
|
14
|
+
| 3000 | <------------- | *.tunnel.yourdomain | ---------> | requests |
|
|
15
15
|
+--------------+ +------------------------+ +----------+
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
1. The client connects to
|
|
19
|
-
2. The relay server assigns a public subdomain (e.g., `abc123.tunnel.
|
|
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
|
-
###
|
|
37
|
+
### 1. Set environment variables (recommended)
|
|
32
38
|
|
|
33
39
|
```bash
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
#
|
|
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
|
-
###
|
|
82
|
+
### With --server flag (no env var needed)
|
|
51
83
|
|
|
52
|
-
```
|
|
53
|
-
|
|
84
|
+
```bash
|
|
85
|
+
npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com
|
|
86
|
+
```
|
|
54
87
|
|
|
55
|
-
|
|
56
|
-
port: 3000,
|
|
57
|
-
apiKey: 'sk_your_key_here',
|
|
58
|
-
});
|
|
88
|
+
### With --server and --subdomain
|
|
59
89
|
|
|
60
|
-
|
|
61
|
-
|
|
90
|
+
```bash
|
|
91
|
+
npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --subdomain myapp
|
|
92
|
+
```
|
|
62
93
|
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
96
|
+
```bash
|
|
97
|
+
npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --api-key sk_your_key
|
|
98
|
+
```
|
|
71
99
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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 (
|
|
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
|
|
147
|
+
## Programmatic API
|
|
94
148
|
|
|
95
|
-
###
|
|
149
|
+
### Basic usage
|
|
96
150
|
|
|
97
|
-
|
|
151
|
+
```typescript
|
|
152
|
+
import { exposeTunnel } from '@gagandeep023/expose-tunnel';
|
|
98
153
|
|
|
99
|
-
|
|
154
|
+
const tunnel = await exposeTunnel({
|
|
155
|
+
port: 3000,
|
|
156
|
+
server: 'wss://tunnel.yourdomain.com',
|
|
157
|
+
apiKey: 'sk_your_key_here',
|
|
158
|
+
});
|
|
100
159
|
|
|
101
|
-
|
|
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
|
-
|
|
162
|
+
// Close when done
|
|
163
|
+
await tunnel.close();
|
|
164
|
+
```
|
|
110
165
|
|
|
111
|
-
###
|
|
166
|
+
### With subdomain
|
|
112
167
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
176
|
+
console.log(tunnel.url);
|
|
177
|
+
// -> https://myapp.tunnel.yourdomain.com
|
|
178
|
+
```
|
|
121
179
|
|
|
122
|
-
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
server_name tunnel.yourdomain.com *.tunnel.yourdomain.com;
|
|
325
|
+
```javascript
|
|
326
|
+
const fs = require('fs');
|
|
327
|
+
const path = require('path');
|
|
211
328
|
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
338
|
+
Start it:
|
|
231
339
|
|
|
232
340
|
```bash
|
|
233
|
-
|
|
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
|
|
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
|
|
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
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uBAAwB;;;ACAxB,gBAAsB;AACtB,uBAAiB;AACjB,yBAA6B;;;ACF7B,IAAM,SAAS;AAEf,IAAM,SAAS;AAAA,EACb,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AACP;AAEO,IAAM,SAAS;AAAA,EACpB,KAAK,KAAmB;AACtB,YAAQ,IAAI,GAAG,OAAO,IAAI,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,GAAG,EAAE;AAAA,EAC7D;AAAA,EAEA,MAAM,KAAmB;AACvB,YAAQ,MAAM,GAAG,OAAO,GAAG,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,GAAG,EAAE;AAAA,EAC9D;AAAA,EAEA,QAAQ,KAAmB;AACzB,YAAQ,IAAI,GAAG,OAAO,KAAK,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,GAAG,EAAE;AAAA,EAC9D;AAAA,EAEA,QAAQ,QAAgB,MAAc,QAAsB;AAC1D,UAAM,cAAc,SAAS,MAAM,OAAO,QAAQ,SAAS,MAAM,OAAO,SAAS,OAAO;AACxF,YAAQ;AAAA,MACN,GAAG,OAAO,GAAG,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,MAAM,IAAI,IAAI,IAAI,WAAW,GAAG,MAAM,GAAG,OAAO,KAAK;AAAA,IAChG;AAAA,EACF;AACF;;;ADxBA,IAAM,yBAAyB;AAC/B,IAAM,uBAAuB;AAEtB,IAAM,eAAN,cAA2B,gCAAa;AAAA,EACrC,KAAuB;AAAA,EACvB;AAAA,EACA,oBAAoB;AAAA,EACpB,oBAA2C;AAAA,EAC3C,SAAS;AAAA,EAEjB,MAAM;AAAA,EACN,YAAY;AAAA,EAEZ,YAAY,SAAwB;AAClC,UAAM;AACN,UAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI;AAC7C,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,oFAAoF;AAAA,IACtG;AACA,SAAK,UAAU;AAAA,MACb,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ,QAAQ;AAAA,MACtB,WAAW,QAAQ,aAAa;AAAA,MAChC;AAAA,MACA,QAAQ,QAAQ,UAAU,QAAQ,IAAI,yBAAyB;AAAA,MAC/D,WAAW,QAAQ,aAAa;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,UAAmC;AACjC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,QAAQ,GAAG,KAAK,QAAQ,MAAM;AACpC,YAAM,UAAkC;AAAA,QACtC,aAAa,KAAK,QAAQ;AAAA,MAC5B;AAEA,UAAI,KAAK,QAAQ,WAAW;AAC1B,gBAAQ,aAAa,IAAI,KAAK,QAAQ;AAAA,MACxC;AAEA,WAAK,KAAK,IAAI,UAAAA,QAAU,OAAO,EAAE,QAAQ,CAAC;AAE1C,YAAM,UAAU,CAAC,QAAqB;AACpC,aAAK,IAAI,mBAAmB;AAC5B,eAAO,IAAI,MAAM,sCAAsC,IAAI,OAAO,EAAE,CAAC;AAAA,MACvE;AAEA,WAAK,GAAG,KAAK,SAAS,OAAO;AAE7B,WAAK,GAAG,KAAK,QAAQ,MAAM;AACzB,aAAK,IAAI,eAAe,SAAS,OAAO;AACxC,aAAK,eAAe,OAAO;AAAA,MAC7B,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,SAAS;AACd,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AACpC,WAAK,oBAAoB;AAAA,IAC3B;AACA,QAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAAA,QAAU,MAAM;AACpD,WAAK,GAAG,MAAM;AAAA,IAChB;AACA,SAAK,KAAK;AACV,SAAK,KAAK,OAAO;AAAA,EACnB;AAAA,EAEQ,eAAe,SAAmD;AACxE,QAAI,CAAC,KAAK,GAAI;AAEd,QAAI,WAAW;AAEf,SAAK,GAAG,GAAG,WAAW,CAAC,SAAS;AAC9B,UAAI;AACF,cAAM,UAAU,KAAK,MAAM,KAAK,SAAS,CAAC;AAE1C,gBAAQ,QAAQ,MAAM;AAAA,UACpB,KAAK;AACH,iBAAK,MAAM,QAAQ;AACnB,iBAAK,YAAY,QAAQ;AACzB,iBAAK,oBAAoB;AACzB,uBAAW;AACX,iBAAK,eAAe;AACpB,oBAAQ,KAAK,eAAe,CAAC;AAC7B;AAAA,UAEF,KAAK;AACH,iBAAK,oBAAoB,QAAQ,OAAO;AACxC;AAAA,UAEF,KAAK;AACH,iBAAK,IAAI,KAAK,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC;AAC9C;AAAA,UAEF,KAAK;AACH,mBAAO,MAAM,iBAAiB,QAAQ,OAAO,EAAE;AAC/C,iBAAK,KAAK,SAAS,IAAI,MAAM,QAAQ,OAAO,CAAC;AAC7C;AAAA,QACJ;AAAA,MACF,QAAQ;AACN,eAAO,MAAM,gCAAgC;AAAA,MAC/C;AAAA,IACF,CAAC;AAED,SAAK,GAAG,GAAG,SAAS,MAAM;AACxB,UAAI,KAAK,mBAAmB;AAC1B,sBAAc,KAAK,iBAAiB;AACpC,aAAK,oBAAoB;AAAA,MAC3B;AAEA,UAAI,CAAC,KAAK,UAAU,UAAU;AAC5B,eAAO,KAAK,6CAA6C;AACzD,aAAK,UAAU;AAAA,MACjB;AAAA,IACF,CAAC;AAED,SAAK,GAAG,GAAG,SAAS,CAAC,QAAQ;AAC3B,aAAO,MAAM,oBAAoB,IAAI,OAAO,EAAE;AAC9C,WAAK,KAAK,SAAS,GAAG;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,oBAAoB,SAAuC;AACvE,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,aAAa,OAAO;AAChD,WAAK,aAAa,QAAQ;AAC1B,WAAK,KAAK,WAAW,QAAQ,QAAQ,QAAQ,MAAM,SAAS,MAAM;AAClE,aAAO,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,SAAS,MAAM;AAAA,IAC9D,SAAS,KAAK;AACZ,YAAM,gBAAgC;AAAA,QACpC,IAAI,QAAQ;AAAA,QACZ,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,OAAO;AAAA,UACX,KAAK,UAAU,EAAE,OAAO,gCAAgC,SAAU,IAAc,QAAQ,CAAC;AAAA,QAC3F,EAAE,SAAS,QAAQ;AAAA,MACrB;AACA,WAAK,aAAa,aAAa;AAC/B,WAAK,KAAK,WAAW,QAAQ,QAAQ,QAAQ,MAAM,GAAG;AACtD,aAAO,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,GAAG;AAAA,IAClD;AAAA,EACF;AAAA,EAEQ,aAAa,SAAiD;AACpE,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,MAAM,UAAU,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,IAAI,GAAG,QAAQ,IAAI;AAChF,YAAM,YAAY,IAAI,IAAI,GAAG;AAE7B,YAAM,UAAkC,EAAE,GAAG,QAAQ,QAAQ;AAE7D,cAAQ,MAAM,IAAI,GAAG,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,IAAI;AAEhE,aAAO,QAAQ,YAAY;AAC3B,aAAO,QAAQ,SAAS;AAExB,YAAM,UAA+B;AAAA,QACnC,UAAU,UAAU;AAAA,QACpB,MAAM,UAAU;AAAA,QAChB,MAAM,UAAU,WAAW,UAAU;AAAA,QACrC,QAAQ,QAAQ;AAAA,QAChB;AAAA,MACF;AAEA,YAAM,WAAW,iBAAAC,QAAK,QAAQ,SAAS,CAAC,aAAa;AACnD,cAAM,SAAmB,CAAC;AAE1B,iBAAS,GAAG,QAAQ,CAAC,UAAkB;AACrC,iBAAO,KAAK,KAAK;AAAA,QACnB,CAAC;AAED,iBAAS,GAAG,OAAO,MAAM;AACvB,gBAAM,OAAO,OAAO,OAAO,MAAM;AACjC,gBAAM,kBAA0C,CAAC;AAEjD,qBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,OAAO,GAAG;AAC3D,gBAAI,OAAO;AACT,8BAAgB,GAAG,IAAI,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI;AAAA,YACnE;AAAA,UACF;AAEA,kBAAQ;AAAA,YACN,IAAI,QAAQ;AAAA,YACZ,QAAQ,SAAS,cAAc;AAAA,YAC/B,SAAS;AAAA,YACT,MAAM,KAAK,SAAS,IAAI,KAAK,SAAS,QAAQ,IAAI;AAAA,UACpD,CAAC;AAAA,QACH,CAAC;AAAA,MACH,CAAC;AAED,eAAS,GAAG,SAAS,MAAM;AAG3B,UAAI,QAAQ,MAAM;AAChB,iBAAS,MAAM,OAAO,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAAA,MACpD;AAEA,eAAS,IAAI;AAAA,IACf,CAAC;AAAA,EACH;AAAA,EAEQ,aAAa,UAAgC;AACnD,QAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAAD,QAAU,MAAM;AACpD,YAAM,UAAqB,EAAE,MAAM,mBAAmB,SAAS;AAC/D,WAAK,GAAG,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,IACtC;AAAA,EACF;AAAA,EAEQ,YAAkB;AACxB,QAAI,KAAK,UAAU,KAAK,qBAAqB,wBAAwB;AACnE,UAAI,KAAK,qBAAqB,wBAAwB;AACpD,eAAO,MAAM,2BAA2B,sBAAsB,uBAAuB;AACrF,aAAK,KAAK,OAAO;AAAA,MACnB;AACA;AAAA,IACF;AAEA,SAAK;AACL,UAAM,QAAQ,uBAAuB,KAAK,IAAI,GAAG,KAAK,oBAAoB,CAAC;AAE3E,WAAO,KAAK,mBAAmB,QAAQ,GAAI,cAAc,KAAK,iBAAiB,IAAI,sBAAsB,MAAM;AAE/G,eAAW,MAAM;AACf,UAAI,KAAK,OAAQ;AAEjB,YAAM,QAAQ,GAAG,KAAK,QAAQ,MAAM;AACpC,YAAM,UAAkC;AAAA,QACtC,aAAa,KAAK,QAAQ;AAAA,MAC5B;AAEA,UAAI,KAAK,WAAW;AAClB,gBAAQ,aAAa,IAAI,KAAK;AAAA,MAChC;AAEA,WAAK,KAAK,IAAI,UAAAA,QAAU,OAAO,EAAE,QAAQ,CAAC;AAE1C,WAAK,GAAG,KAAK,QAAQ,MAAM;AACzB,eAAO,QAAQ,cAAc;AAC7B,aAAK,eAAe,MAAM;AAAA,QAAC,CAAC;AAAA,MAC9B,CAAC;AAED,WAAK,GAAG,KAAK,SAAS,MAAM;AAC1B,aAAK,UAAU;AAAA,MACjB,CAAC;AAAA,IACH,GAAG,KAAK;AAAA,EACV;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AAAA,IACtC;AAAA,EAGF;AAAA,EAEQ,iBAAiC;AACvC,WAAO;AAAA,MACL,KAAK,KAAK;AAAA,MACV,WAAW,KAAK;AAAA,MAChB,OAAO,MAAM,KAAK,MAAM;AAAA,MACxB,IAAI,CAAC,OAAe,YAA0C;AAC5D,aAAK,GAAG,OAAO,OAAO;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,aAAa,SAAiD;AAClF,QAAM,SAAS,IAAI,aAAa,OAAO;AACvC,SAAO,OAAO,QAAQ;AACxB;;;ADjRA,IAAM,UAAU,IAAI,yBAAQ;AAE5B,QACG,KAAK,eAAe,EACpB,YAAY,gEAAgE,EAC5E,QAAQ,OAAO,EACf,eAAe,uBAAuB,sBAAsB,EAC5D,OAAO,0BAA0B,8BAA8B,EAC/D,OAAO,kBAAkB,kEAAkE,EAC3F,OAAO,mBAAmB,gDAAgD,EAC1E,OAAO,uBAAuB,8BAA8B,WAAW,EACvE,OAAO,OAAO,SAAS;AACtB,QAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,MAAI,CAAC,QAAQ;AACX,WAAO,MAAM,oFAAoF;AACjG,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,MAAI,CAAC,QAAQ;AACX,WAAO,MAAM,uEAAuE;AACpF,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,OAAO,SAAS,KAAK,MAAM,EAAE;AACnC,MAAI,MAAM,IAAI,KAAK,OAAO,KAAK,OAAO,OAAO;AAC3C,WAAO,MAAM,mDAAmD;AAChE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,aAAa;AAAA,MAChC;AAAA,MACA,WAAW,KAAK;AAAA,MAChB;AAAA,MACA;AAAA,MACA,WAAW,KAAK;AAAA,IAClB,CAAC;AAED,WAAO,QAAQ,qBAAqB;AACpC,WAAO,KAAK,eAAe,OAAO,GAAG,EAAE;AACvC,WAAO,KAAK,yBAAyB,KAAK,SAAS,IAAI,IAAI,EAAE;AAC7D,WAAO,KAAK,qCAAqC;AAEjD,WAAO,GAAG,SAAS,IAAI,SAAoB;AACzC,YAAM,MAAM,KAAK,CAAC;AAClB,aAAO,MAAM,IAAI,OAAO;AAAA,IAC1B,CAAC;AAED,UAAM,WAAW,YAA2B;AAC1C,aAAO,KAAK,qBAAqB;AACjC,YAAM,OAAO,MAAM;AACnB,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,YAAQ,GAAG,UAAU,QAAQ;AAC7B,YAAQ,GAAG,WAAW,QAAQ;AAAA,EAChC,SAAS,KAAK;AACZ,WAAO,MAAM,+BAAgC,IAAc,OAAO,EAAE;AACpE,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAEH,QAAQ,MAAM;","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
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,gBAAsB;AACtB,uBAAiB;AACjB,yBAA6B;;;ACF7B,IAAM,SAAS;AAEf,IAAM,SAAS;AAAA,EACb,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AACP;AAEO,IAAM,SAAS;AAAA,EACpB,KAAK,KAAmB;AACtB,YAAQ,IAAI,GAAG,OAAO,IAAI,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,GAAG,EAAE;AAAA,EAC7D;AAAA,EAEA,MAAM,KAAmB;AACvB,YAAQ,MAAM,GAAG,OAAO,GAAG,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,GAAG,EAAE;AAAA,EAC9D;AAAA,EAEA,QAAQ,KAAmB;AACzB,YAAQ,IAAI,GAAG,OAAO,KAAK,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,GAAG,EAAE;AAAA,EAC9D;AAAA,EAEA,QAAQ,QAAgB,MAAc,QAAsB;AAC1D,UAAM,cAAc,SAAS,MAAM,OAAO,QAAQ,SAAS,MAAM,OAAO,SAAS,OAAO;AACxF,YAAQ;AAAA,MACN,GAAG,OAAO,GAAG,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,MAAM,IAAI,IAAI,IAAI,WAAW,GAAG,MAAM,GAAG,OAAO,KAAK;AAAA,IAChG;AAAA,EACF;AACF;;;ADxBA,IAAM,iBAAiB;AACvB,IAAM,yBAAyB;AAC/B,IAAM,uBAAuB;AAEtB,IAAM,eAAN,cAA2B,gCAAa;AAAA,EACrC,KAAuB;AAAA,EACvB;AAAA,EACA,oBAAoB;AAAA,EACpB,oBAA2C;AAAA,EAC3C,SAAS;AAAA,EAEjB,MAAM;AAAA,EACN,YAAY;AAAA,EAEZ,YAAY,SAAwB;AAClC,UAAM;AACN,SAAK,UAAU;AAAA,MACb,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ,QAAQ;AAAA,MACtB,WAAW,QAAQ,aAAa;AAAA,MAChC,QAAQ,QAAQ,UAAU;AAAA,MAC1B,QAAQ,QAAQ,UAAU,QAAQ,IAAI,yBAAyB;AAAA,MAC/D,WAAW,QAAQ,aAAa;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,UAAmC;AACjC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,QAAQ,GAAG,KAAK,QAAQ,MAAM;AACpC,YAAM,UAAkC;AAAA,QACtC,aAAa,KAAK,QAAQ;AAAA,MAC5B;AAEA,UAAI,KAAK,QAAQ,WAAW;AAC1B,gBAAQ,aAAa,IAAI,KAAK,QAAQ;AAAA,MACxC;AAEA,WAAK,KAAK,IAAI,UAAAA,QAAU,OAAO,EAAE,QAAQ,CAAC;AAE1C,YAAM,UAAU,CAAC,QAAqB;AACpC,aAAK,IAAI,mBAAmB;AAC5B,eAAO,IAAI,MAAM,sCAAsC,IAAI,OAAO,EAAE,CAAC;AAAA,MACvE;AAEA,WAAK,GAAG,KAAK,SAAS,OAAO;AAE7B,WAAK,GAAG,KAAK,QAAQ,MAAM;AACzB,aAAK,IAAI,eAAe,SAAS,OAAO;AACxC,aAAK,eAAe,OAAO;AAAA,MAC7B,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,SAAS;AACd,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AACpC,WAAK,oBAAoB;AAAA,IAC3B;AACA,QAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAAA,QAAU,MAAM;AACpD,WAAK,GAAG,MAAM;AAAA,IAChB;AACA,SAAK,KAAK;AACV,SAAK,KAAK,OAAO;AAAA,EACnB;AAAA,EAEQ,eAAe,SAAmD;AACxE,QAAI,CAAC,KAAK,GAAI;AAEd,QAAI,WAAW;AAEf,SAAK,GAAG,GAAG,WAAW,CAAC,SAAS;AAC9B,UAAI;AACF,cAAM,UAAU,KAAK,MAAM,KAAK,SAAS,CAAC;AAE1C,gBAAQ,QAAQ,MAAM;AAAA,UACpB,KAAK;AACH,iBAAK,MAAM,QAAQ;AACnB,iBAAK,YAAY,QAAQ;AACzB,iBAAK,oBAAoB;AACzB,uBAAW;AACX,iBAAK,eAAe;AACpB,oBAAQ,KAAK,eAAe,CAAC;AAC7B;AAAA,UAEF,KAAK;AACH,iBAAK,oBAAoB,QAAQ,OAAO;AACxC;AAAA,UAEF,KAAK;AACH,iBAAK,IAAI,KAAK,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC;AAC9C;AAAA,UAEF,KAAK;AACH,mBAAO,MAAM,iBAAiB,QAAQ,OAAO,EAAE;AAC/C,iBAAK,KAAK,SAAS,IAAI,MAAM,QAAQ,OAAO,CAAC;AAC7C;AAAA,QACJ;AAAA,MACF,QAAQ;AACN,eAAO,MAAM,gCAAgC;AAAA,MAC/C;AAAA,IACF,CAAC;AAED,SAAK,GAAG,GAAG,SAAS,MAAM;AACxB,UAAI,KAAK,mBAAmB;AAC1B,sBAAc,KAAK,iBAAiB;AACpC,aAAK,oBAAoB;AAAA,MAC3B;AAEA,UAAI,CAAC,KAAK,UAAU,UAAU;AAC5B,eAAO,KAAK,6CAA6C;AACzD,aAAK,UAAU;AAAA,MACjB;AAAA,IACF,CAAC;AAED,SAAK,GAAG,GAAG,SAAS,CAAC,QAAQ;AAC3B,aAAO,MAAM,oBAAoB,IAAI,OAAO,EAAE;AAC9C,WAAK,KAAK,SAAS,GAAG;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,oBAAoB,SAAuC;AACvE,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,aAAa,OAAO;AAChD,WAAK,aAAa,QAAQ;AAC1B,WAAK,KAAK,WAAW,QAAQ,QAAQ,QAAQ,MAAM,SAAS,MAAM;AAClE,aAAO,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,SAAS,MAAM;AAAA,IAC9D,SAAS,KAAK;AACZ,YAAM,gBAAgC;AAAA,QACpC,IAAI,QAAQ;AAAA,QACZ,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,OAAO;AAAA,UACX,KAAK,UAAU,EAAE,OAAO,gCAAgC,SAAU,IAAc,QAAQ,CAAC;AAAA,QAC3F,EAAE,SAAS,QAAQ;AAAA,MACrB;AACA,WAAK,aAAa,aAAa;AAC/B,WAAK,KAAK,WAAW,QAAQ,QAAQ,QAAQ,MAAM,GAAG;AACtD,aAAO,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,GAAG;AAAA,IAClD;AAAA,EACF;AAAA,EAEQ,aAAa,SAAiD;AACpE,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,MAAM,UAAU,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,IAAI,GAAG,QAAQ,IAAI;AAChF,YAAM,YAAY,IAAI,IAAI,GAAG;AAE7B,YAAM,UAAkC,EAAE,GAAG,QAAQ,QAAQ;AAE7D,cAAQ,MAAM,IAAI,GAAG,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,IAAI;AAEhE,aAAO,QAAQ,YAAY;AAC3B,aAAO,QAAQ,SAAS;AAExB,YAAM,UAA+B;AAAA,QACnC,UAAU,UAAU;AAAA,QACpB,MAAM,UAAU;AAAA,QAChB,MAAM,UAAU,WAAW,UAAU;AAAA,QACrC,QAAQ,QAAQ;AAAA,QAChB;AAAA,MACF;AAEA,YAAM,WAAW,iBAAAC,QAAK,QAAQ,SAAS,CAAC,aAAa;AACnD,cAAM,SAAmB,CAAC;AAE1B,iBAAS,GAAG,QAAQ,CAAC,UAAkB;AACrC,iBAAO,KAAK,KAAK;AAAA,QACnB,CAAC;AAED,iBAAS,GAAG,OAAO,MAAM;AACvB,gBAAM,OAAO,OAAO,OAAO,MAAM;AACjC,gBAAM,kBAA0C,CAAC;AAEjD,qBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,OAAO,GAAG;AAC3D,gBAAI,OAAO;AACT,8BAAgB,GAAG,IAAI,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI;AAAA,YACnE;AAAA,UACF;AAEA,kBAAQ;AAAA,YACN,IAAI,QAAQ;AAAA,YACZ,QAAQ,SAAS,cAAc;AAAA,YAC/B,SAAS;AAAA,YACT,MAAM,KAAK,SAAS,IAAI,KAAK,SAAS,QAAQ,IAAI;AAAA,UACpD,CAAC;AAAA,QACH,CAAC;AAAA,MACH,CAAC;AAED,eAAS,GAAG,SAAS,MAAM;AAG3B,UAAI,QAAQ,MAAM;AAChB,iBAAS,MAAM,OAAO,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAAA,MACpD;AAEA,eAAS,IAAI;AAAA,IACf,CAAC;AAAA,EACH;AAAA,EAEQ,aAAa,UAAgC;AACnD,QAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAAD,QAAU,MAAM;AACpD,YAAM,UAAqB,EAAE,MAAM,mBAAmB,SAAS;AAC/D,WAAK,GAAG,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,IACtC;AAAA,EACF;AAAA,EAEQ,YAAkB;AACxB,QAAI,KAAK,UAAU,KAAK,qBAAqB,wBAAwB;AACnE,UAAI,KAAK,qBAAqB,wBAAwB;AACpD,eAAO,MAAM,2BAA2B,sBAAsB,uBAAuB;AACrF,aAAK,KAAK,OAAO;AAAA,MACnB;AACA;AAAA,IACF;AAEA,SAAK;AACL,UAAM,QAAQ,uBAAuB,KAAK,IAAI,GAAG,KAAK,oBAAoB,CAAC;AAE3E,WAAO,KAAK,mBAAmB,QAAQ,GAAI,cAAc,KAAK,iBAAiB,IAAI,sBAAsB,MAAM;AAE/G,eAAW,MAAM;AACf,UAAI,KAAK,OAAQ;AAEjB,YAAM,QAAQ,GAAG,KAAK,QAAQ,MAAM;AACpC,YAAM,UAAkC;AAAA,QACtC,aAAa,KAAK,QAAQ;AAAA,MAC5B;AAEA,UAAI,KAAK,WAAW;AAClB,gBAAQ,aAAa,IAAI,KAAK;AAAA,MAChC;AAEA,WAAK,KAAK,IAAI,UAAAA,QAAU,OAAO,EAAE,QAAQ,CAAC;AAE1C,WAAK,GAAG,KAAK,QAAQ,MAAM;AACzB,eAAO,QAAQ,cAAc;AAC7B,aAAK,eAAe,MAAM;AAAA,QAAC,CAAC;AAAA,MAC9B,CAAC;AAED,WAAK,GAAG,KAAK,SAAS,MAAM;AAC1B,aAAK,UAAU;AAAA,MACjB,CAAC;AAAA,IACH,GAAG,KAAK;AAAA,EACV;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AAAA,IACtC;AAAA,EAGF;AAAA,EAEQ,iBAAiC;AACvC,WAAO;AAAA,MACL,KAAK,KAAK;AAAA,MACV,WAAW,KAAK;AAAA,MAChB,OAAO,MAAM,KAAK,MAAM;AAAA,MACxB,IAAI,CAAC,OAAe,YAA0C;AAC5D,aAAK,GAAG,OAAO,OAAO;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,aAAa,SAAiD;AAClF,QAAM,SAAS,IAAI,aAAa,OAAO;AACvC,SAAO,OAAO,QAAQ;AACxB;","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
|
|
55
|
+
server,
|
|
53
56
|
apiKey: options.apiKey || process.env.EXPOSE_TUNNEL_API_KEY || "",
|
|
54
57
|
localHost: options.localHost || "localhost"
|
|
55
58
|
};
|
package/dist/index.mjs.map
CHANGED
|
@@ -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":";AAAA,OAAO,eAAe;AACtB,OAAO,UAAU;AACjB,SAAS,oBAAoB;;;ACF7B,IAAM,SAAS;AAEf,IAAM,SAAS;AAAA,EACb,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AACP;AAEO,IAAM,SAAS;AAAA,EACpB,KAAK,KAAmB;AACtB,YAAQ,IAAI,GAAG,OAAO,IAAI,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,GAAG,EAAE;AAAA,EAC7D;AAAA,EAEA,MAAM,KAAmB;AACvB,YAAQ,MAAM,GAAG,OAAO,GAAG,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,GAAG,EAAE;AAAA,EAC9D;AAAA,EAEA,QAAQ,KAAmB;AACzB,YAAQ,IAAI,GAAG,OAAO,KAAK,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,GAAG,EAAE;AAAA,EAC9D;AAAA,EAEA,QAAQ,QAAgB,MAAc,QAAsB;AAC1D,UAAM,cAAc,SAAS,MAAM,OAAO,QAAQ,SAAS,MAAM,OAAO,SAAS,OAAO;AACxF,YAAQ;AAAA,MACN,GAAG,OAAO,GAAG,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,MAAM,IAAI,IAAI,IAAI,WAAW,GAAG,MAAM,GAAG,OAAO,KAAK;AAAA,IAChG;AAAA,EACF;AACF;;;ADxBA,IAAM,yBAAyB;AAC/B,IAAM,uBAAuB;AAEtB,IAAM,eAAN,cAA2B,aAAa;AAAA,EACrC,KAAuB;AAAA,EACvB;AAAA,EACA,oBAAoB;AAAA,EACpB,oBAA2C;AAAA,EAC3C,SAAS;AAAA,EAEjB,MAAM;AAAA,EACN,YAAY;AAAA,EAEZ,YAAY,SAAwB;AAClC,UAAM;AACN,UAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI;AAC7C,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,oFAAoF;AAAA,IACtG;AACA,SAAK,UAAU;AAAA,MACb,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ,QAAQ;AAAA,MACtB,WAAW,QAAQ,aAAa;AAAA,MAChC;AAAA,MACA,QAAQ,QAAQ,UAAU,QAAQ,IAAI,yBAAyB;AAAA,MAC/D,WAAW,QAAQ,aAAa;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,UAAmC;AACjC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,QAAQ,GAAG,KAAK,QAAQ,MAAM;AACpC,YAAM,UAAkC;AAAA,QACtC,aAAa,KAAK,QAAQ;AAAA,MAC5B;AAEA,UAAI,KAAK,QAAQ,WAAW;AAC1B,gBAAQ,aAAa,IAAI,KAAK,QAAQ;AAAA,MACxC;AAEA,WAAK,KAAK,IAAI,UAAU,OAAO,EAAE,QAAQ,CAAC;AAE1C,YAAM,UAAU,CAAC,QAAqB;AACpC,aAAK,IAAI,mBAAmB;AAC5B,eAAO,IAAI,MAAM,sCAAsC,IAAI,OAAO,EAAE,CAAC;AAAA,MACvE;AAEA,WAAK,GAAG,KAAK,SAAS,OAAO;AAE7B,WAAK,GAAG,KAAK,QAAQ,MAAM;AACzB,aAAK,IAAI,eAAe,SAAS,OAAO;AACxC,aAAK,eAAe,OAAO;AAAA,MAC7B,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,SAAS;AACd,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AACpC,WAAK,oBAAoB;AAAA,IAC3B;AACA,QAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,MAAM;AACpD,WAAK,GAAG,MAAM;AAAA,IAChB;AACA,SAAK,KAAK;AACV,SAAK,KAAK,OAAO;AAAA,EACnB;AAAA,EAEQ,eAAe,SAAmD;AACxE,QAAI,CAAC,KAAK,GAAI;AAEd,QAAI,WAAW;AAEf,SAAK,GAAG,GAAG,WAAW,CAAC,SAAS;AAC9B,UAAI;AACF,cAAM,UAAU,KAAK,MAAM,KAAK,SAAS,CAAC;AAE1C,gBAAQ,QAAQ,MAAM;AAAA,UACpB,KAAK;AACH,iBAAK,MAAM,QAAQ;AACnB,iBAAK,YAAY,QAAQ;AACzB,iBAAK,oBAAoB;AACzB,uBAAW;AACX,iBAAK,eAAe;AACpB,oBAAQ,KAAK,eAAe,CAAC;AAC7B;AAAA,UAEF,KAAK;AACH,iBAAK,oBAAoB,QAAQ,OAAO;AACxC;AAAA,UAEF,KAAK;AACH,iBAAK,IAAI,KAAK,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC;AAC9C;AAAA,UAEF,KAAK;AACH,mBAAO,MAAM,iBAAiB,QAAQ,OAAO,EAAE;AAC/C,iBAAK,KAAK,SAAS,IAAI,MAAM,QAAQ,OAAO,CAAC;AAC7C;AAAA,QACJ;AAAA,MACF,QAAQ;AACN,eAAO,MAAM,gCAAgC;AAAA,MAC/C;AAAA,IACF,CAAC;AAED,SAAK,GAAG,GAAG,SAAS,MAAM;AACxB,UAAI,KAAK,mBAAmB;AAC1B,sBAAc,KAAK,iBAAiB;AACpC,aAAK,oBAAoB;AAAA,MAC3B;AAEA,UAAI,CAAC,KAAK,UAAU,UAAU;AAC5B,eAAO,KAAK,6CAA6C;AACzD,aAAK,UAAU;AAAA,MACjB;AAAA,IACF,CAAC;AAED,SAAK,GAAG,GAAG,SAAS,CAAC,QAAQ;AAC3B,aAAO,MAAM,oBAAoB,IAAI,OAAO,EAAE;AAC9C,WAAK,KAAK,SAAS,GAAG;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,oBAAoB,SAAuC;AACvE,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,aAAa,OAAO;AAChD,WAAK,aAAa,QAAQ;AAC1B,WAAK,KAAK,WAAW,QAAQ,QAAQ,QAAQ,MAAM,SAAS,MAAM;AAClE,aAAO,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,SAAS,MAAM;AAAA,IAC9D,SAAS,KAAK;AACZ,YAAM,gBAAgC;AAAA,QACpC,IAAI,QAAQ;AAAA,QACZ,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,OAAO;AAAA,UACX,KAAK,UAAU,EAAE,OAAO,gCAAgC,SAAU,IAAc,QAAQ,CAAC;AAAA,QAC3F,EAAE,SAAS,QAAQ;AAAA,MACrB;AACA,WAAK,aAAa,aAAa;AAC/B,WAAK,KAAK,WAAW,QAAQ,QAAQ,QAAQ,MAAM,GAAG;AACtD,aAAO,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,GAAG;AAAA,IAClD;AAAA,EACF;AAAA,EAEQ,aAAa,SAAiD;AACpE,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,MAAM,UAAU,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,IAAI,GAAG,QAAQ,IAAI;AAChF,YAAM,YAAY,IAAI,IAAI,GAAG;AAE7B,YAAM,UAAkC,EAAE,GAAG,QAAQ,QAAQ;AAE7D,cAAQ,MAAM,IAAI,GAAG,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,IAAI;AAEhE,aAAO,QAAQ,YAAY;AAC3B,aAAO,QAAQ,SAAS;AAExB,YAAM,UAA+B;AAAA,QACnC,UAAU,UAAU;AAAA,QACpB,MAAM,UAAU;AAAA,QAChB,MAAM,UAAU,WAAW,UAAU;AAAA,QACrC,QAAQ,QAAQ;AAAA,QAChB;AAAA,MACF;AAEA,YAAM,WAAW,KAAK,QAAQ,SAAS,CAAC,aAAa;AACnD,cAAM,SAAmB,CAAC;AAE1B,iBAAS,GAAG,QAAQ,CAAC,UAAkB;AACrC,iBAAO,KAAK,KAAK;AAAA,QACnB,CAAC;AAED,iBAAS,GAAG,OAAO,MAAM;AACvB,gBAAM,OAAO,OAAO,OAAO,MAAM;AACjC,gBAAM,kBAA0C,CAAC;AAEjD,qBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,OAAO,GAAG;AAC3D,gBAAI,OAAO;AACT,8BAAgB,GAAG,IAAI,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI;AAAA,YACnE;AAAA,UACF;AAEA,kBAAQ;AAAA,YACN,IAAI,QAAQ;AAAA,YACZ,QAAQ,SAAS,cAAc;AAAA,YAC/B,SAAS;AAAA,YACT,MAAM,KAAK,SAAS,IAAI,KAAK,SAAS,QAAQ,IAAI;AAAA,UACpD,CAAC;AAAA,QACH,CAAC;AAAA,MACH,CAAC;AAED,eAAS,GAAG,SAAS,MAAM;AAG3B,UAAI,QAAQ,MAAM;AAChB,iBAAS,MAAM,OAAO,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAAA,MACpD;AAEA,eAAS,IAAI;AAAA,IACf,CAAC;AAAA,EACH;AAAA,EAEQ,aAAa,UAAgC;AACnD,QAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,MAAM;AACpD,YAAM,UAAqB,EAAE,MAAM,mBAAmB,SAAS;AAC/D,WAAK,GAAG,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,IACtC;AAAA,EACF;AAAA,EAEQ,YAAkB;AACxB,QAAI,KAAK,UAAU,KAAK,qBAAqB,wBAAwB;AACnE,UAAI,KAAK,qBAAqB,wBAAwB;AACpD,eAAO,MAAM,2BAA2B,sBAAsB,uBAAuB;AACrF,aAAK,KAAK,OAAO;AAAA,MACnB;AACA;AAAA,IACF;AAEA,SAAK;AACL,UAAM,QAAQ,uBAAuB,KAAK,IAAI,GAAG,KAAK,oBAAoB,CAAC;AAE3E,WAAO,KAAK,mBAAmB,QAAQ,GAAI,cAAc,KAAK,iBAAiB,IAAI,sBAAsB,MAAM;AAE/G,eAAW,MAAM;AACf,UAAI,KAAK,OAAQ;AAEjB,YAAM,QAAQ,GAAG,KAAK,QAAQ,MAAM;AACpC,YAAM,UAAkC;AAAA,QACtC,aAAa,KAAK,QAAQ;AAAA,MAC5B;AAEA,UAAI,KAAK,WAAW;AAClB,gBAAQ,aAAa,IAAI,KAAK;AAAA,MAChC;AAEA,WAAK,KAAK,IAAI,UAAU,OAAO,EAAE,QAAQ,CAAC;AAE1C,WAAK,GAAG,KAAK,QAAQ,MAAM;AACzB,eAAO,QAAQ,cAAc;AAC7B,aAAK,eAAe,MAAM;AAAA,QAAC,CAAC;AAAA,MAC9B,CAAC;AAED,WAAK,GAAG,KAAK,SAAS,MAAM;AAC1B,aAAK,UAAU;AAAA,MACjB,CAAC;AAAA,IACH,GAAG,KAAK;AAAA,EACV;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AAAA,IACtC;AAAA,EAGF;AAAA,EAEQ,iBAAiC;AACvC,WAAO;AAAA,MACL,KAAK,KAAK;AAAA,MACV,WAAW,KAAK;AAAA,MAChB,OAAO,MAAM,KAAK,MAAM;AAAA,MACxB,IAAI,CAAC,OAAe,YAA0C;AAC5D,aAAK,GAAG,OAAO,OAAO;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,aAAa,SAAiD;AAClF,QAAM,SAAS,IAAI,aAAa,OAAO;AACvC,SAAO,OAAO,QAAQ;AACxB;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gagandeep023/expose-tunnel",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Self-hosted tunnel to expose local servers to the internet
|
|
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",
|