untunneled.dev 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 untunneled.dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,846 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/start.ts
7
+ import { render } from "ink";
8
+ import React5 from "react";
9
+
10
+ // src/ui/TunnelUI.tsx
11
+ import { useState as useState2, useEffect as useEffect2 } from "react";
12
+ import { Box as Box4, Text as Text4, Newline, useApp } from "ink";
13
+
14
+ // src/ui/QRCode.tsx
15
+ import { useState, useEffect } from "react";
16
+ import { Box, Text } from "ink";
17
+ import qrcode from "qrcode-terminal";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ var QRCode = ({ url }) => {
20
+ const [qrString, setQrString] = useState("");
21
+ useEffect(() => {
22
+ qrcode.generate(url, { small: true }, (qr) => {
23
+ setQrString(qr);
24
+ });
25
+ }, [url]);
26
+ if (!qrString) {
27
+ return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Generating QR code..." }) });
28
+ }
29
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
30
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Scan with mobile:" }),
31
+ /* @__PURE__ */ jsx(Text, { children: qrString })
32
+ ] });
33
+ };
34
+
35
+ // src/ui/RequestLog.tsx
36
+ import "react";
37
+ import { Box as Box2, Text as Text2 } from "ink";
38
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
39
+ var getStatusColor = (status) => {
40
+ if (status >= 500) return "red";
41
+ if (status >= 400) return "yellow";
42
+ if (status >= 300) return "blue";
43
+ return "green";
44
+ };
45
+ var getMethodColor = (method) => {
46
+ switch (method) {
47
+ case "GET":
48
+ return "green";
49
+ case "POST":
50
+ return "yellow";
51
+ case "PUT":
52
+ return "blue";
53
+ case "DELETE":
54
+ return "red";
55
+ case "PATCH":
56
+ return "magenta";
57
+ default:
58
+ return "white";
59
+ }
60
+ };
61
+ var RequestLog = ({ requests }) => {
62
+ if (requests.length === 0) {
63
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " Waiting for requests..." });
64
+ }
65
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: requests.map((req) => /* @__PURE__ */ jsxs2(Box2, { gap: 1, children: [
66
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: req.timestamp }),
67
+ /* @__PURE__ */ jsx2(Box2, { width: 7, children: /* @__PURE__ */ jsx2(Text2, { color: getMethodColor(req.method), children: req.method.padEnd(6) }) }),
68
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, children: /* @__PURE__ */ jsx2(Text2, { children: req.path.length > 40 ? req.path.substring(0, 37) + "..." : req.path }) }),
69
+ /* @__PURE__ */ jsx2(Box2, { width: 5, children: /* @__PURE__ */ jsx2(Text2, { color: getStatusColor(req.status), children: req.status }) }),
70
+ /* @__PURE__ */ jsx2(Box2, { width: 8, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
71
+ req.duration,
72
+ "ms"
73
+ ] }) }),
74
+ /* @__PURE__ */ jsx2(Box2, { width: 7, children: /* @__PURE__ */ jsxs2(Text2, { color: req.type === "p2p" ? "green" : "yellow", children: [
75
+ "[",
76
+ req.type.toUpperCase(),
77
+ "]"
78
+ ] }) })
79
+ ] }, req.id)) });
80
+ };
81
+
82
+ // src/ui/ConnectionStatus.tsx
83
+ import "react";
84
+ import { Box as Box3, Text as Text3 } from "ink";
85
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
86
+ var getStatusIndicator = (status) => {
87
+ switch (status) {
88
+ case "connected":
89
+ return { color: "green", symbol: "\u25CF" };
90
+ case "connecting":
91
+ return { color: "yellow", symbol: "\u25D0" };
92
+ case "disconnected":
93
+ return { color: "red", symbol: "\u25CB" };
94
+ case "error":
95
+ return { color: "red", symbol: "\u2717" };
96
+ }
97
+ };
98
+ var getModeLabel = (mode, p2pConnected) => {
99
+ if (mode === "relay") return "HTTP Relay";
100
+ if (mode === "p2p-only") return p2pConnected ? "P2P Direct" : "P2P (connecting...)";
101
+ return p2pConnected ? "P2P + Relay Fallback" : "Relay (P2P pending...)";
102
+ };
103
+ var ConnectionStatus = ({
104
+ status,
105
+ mode,
106
+ p2pConnected,
107
+ relayConnected
108
+ }) => {
109
+ const indicator = getStatusIndicator(status);
110
+ const modeLabel = getModeLabel(mode, p2pConnected);
111
+ return /* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
112
+ /* @__PURE__ */ jsxs3(Box3, { children: [
113
+ /* @__PURE__ */ jsx3(Text3, { color: indicator.color, children: indicator.symbol }),
114
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
115
+ /* @__PURE__ */ jsx3(Text3, { color: indicator.color, children: status.toUpperCase() })
116
+ ] }),
117
+ /* @__PURE__ */ jsxs3(Box3, { children: [
118
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Mode: " }),
119
+ /* @__PURE__ */ jsx3(Text3, { color: p2pConnected ? "green" : "yellow", children: modeLabel })
120
+ ] }),
121
+ mode !== "relay" && /* @__PURE__ */ jsxs3(Box3, { children: [
122
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "P2P: " }),
123
+ /* @__PURE__ */ jsx3(Text3, { color: p2pConnected ? "green" : "yellow", children: p2pConnected ? "Active" : "Pending" })
124
+ ] }),
125
+ /* @__PURE__ */ jsxs3(Box3, { children: [
126
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Relay: " }),
127
+ /* @__PURE__ */ jsx3(Text3, { color: relayConnected ? "green" : "red", children: relayConnected ? "Connected" : "Disconnected" })
128
+ ] })
129
+ ] });
130
+ };
131
+
132
+ // src/ui/TunnelUI.tsx
133
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
134
+ var MAX_LOGS = 10;
135
+ var TunnelUI = ({
136
+ tunnelUrl,
137
+ localPort,
138
+ showQR,
139
+ mode,
140
+ relayTunnel,
141
+ p2pTunnel,
142
+ p2pError
143
+ }) => {
144
+ useApp();
145
+ const [requests, setRequests] = useState2([]);
146
+ const [stats, setStats] = useState2({
147
+ total: 0,
148
+ p2p: 0,
149
+ relay: 0,
150
+ startTime: Date.now()
151
+ });
152
+ const [relayConnected, setRelayConnected] = useState2(relayTunnel.isConnected());
153
+ const [p2pConnected, setP2pConnected] = useState2(p2pTunnel?.isConnected() ?? false);
154
+ useEffect2(() => {
155
+ const handleRequest = (req) => {
156
+ setRequests((prev) => [...prev.slice(-(MAX_LOGS - 1)), req]);
157
+ setStats((prev) => ({
158
+ ...prev,
159
+ total: prev.total + 1,
160
+ p2p: prev.p2p + (req.type === "p2p" ? 1 : 0),
161
+ relay: prev.relay + (req.type === "relay" ? 1 : 0)
162
+ }));
163
+ };
164
+ const handleRelayConnected = () => setRelayConnected(true);
165
+ const handleRelayDisconnected = () => setRelayConnected(false);
166
+ const handleP2PConnected = () => setP2pConnected(true);
167
+ const handleP2PDisconnected = () => setP2pConnected(false);
168
+ relayTunnel.on("request", handleRequest);
169
+ relayTunnel.on("connected", handleRelayConnected);
170
+ relayTunnel.on("disconnected", handleRelayDisconnected);
171
+ if (p2pTunnel) {
172
+ p2pTunnel.on("request", handleRequest);
173
+ p2pTunnel.on("connected", handleP2PConnected);
174
+ p2pTunnel.on("disconnected", handleP2PDisconnected);
175
+ }
176
+ return () => {
177
+ relayTunnel.off("request", handleRequest);
178
+ relayTunnel.off("connected", handleRelayConnected);
179
+ relayTunnel.off("disconnected", handleRelayDisconnected);
180
+ if (p2pTunnel) {
181
+ p2pTunnel.off("request", handleRequest);
182
+ p2pTunnel.off("connected", handleP2PConnected);
183
+ p2pTunnel.off("disconnected", handleP2PDisconnected);
184
+ }
185
+ };
186
+ }, [relayTunnel, p2pTunnel]);
187
+ const connectionStatus = relayConnected || p2pConnected ? "connected" : "connecting";
188
+ const borderColor = p2pConnected ? "green" : relayConnected ? "yellow" : "gray";
189
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", padding: 1, children: [
190
+ /* @__PURE__ */ jsxs4(Box4, { borderStyle: "round", borderColor, padding: 1, flexDirection: "column", children: [
191
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, color: borderColor, children: [
192
+ p2pConnected ? "\u{1F512}" : "\u26A1",
193
+ " untunneled.dev"
194
+ ] }),
195
+ /* @__PURE__ */ jsx4(Newline, {}),
196
+ /* @__PURE__ */ jsx4(
197
+ ConnectionStatus,
198
+ {
199
+ status: connectionStatus,
200
+ mode,
201
+ p2pConnected,
202
+ relayConnected
203
+ }
204
+ ),
205
+ /* @__PURE__ */ jsx4(Newline, {}),
206
+ /* @__PURE__ */ jsxs4(Box4, { children: [
207
+ /* @__PURE__ */ jsx4(Text4, { children: "URL: " }),
208
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: tunnelUrl })
209
+ ] }),
210
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
211
+ "\u2192 localhost:",
212
+ localPort
213
+ ] }) })
214
+ ] }),
215
+ p2pError && mode === "p2p-only" && /* @__PURE__ */ jsxs4(
216
+ Box4,
217
+ {
218
+ marginTop: 1,
219
+ borderStyle: "single",
220
+ borderColor: "red",
221
+ padding: 1,
222
+ flexDirection: "column",
223
+ children: [
224
+ /* @__PURE__ */ jsx4(Text4, { color: "red", bold: true, children: "P2P Connection Failed" }),
225
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: p2pError.message }),
226
+ /* @__PURE__ */ jsx4(Newline, {}),
227
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Use --relay-fallback for guaranteed compatibility" })
228
+ ]
229
+ }
230
+ ),
231
+ showQR && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(QRCode, { url: tunnelUrl }) }),
232
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, gap: 2, children: [
233
+ /* @__PURE__ */ jsxs4(Text4, { children: [
234
+ "Requests: ",
235
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: stats.total })
236
+ ] }),
237
+ mode !== "relay" && stats.total > 0 && /* @__PURE__ */ jsxs4(Fragment, { children: [
238
+ /* @__PURE__ */ jsxs4(Text4, { children: [
239
+ "P2P: ",
240
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: stats.p2p })
241
+ ] }),
242
+ /* @__PURE__ */ jsxs4(Text4, { children: [
243
+ "Relay: ",
244
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: stats.relay })
245
+ ] }),
246
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
247
+ "(",
248
+ Math.round(stats.p2p / stats.total * 100),
249
+ "% private)"
250
+ ] })
251
+ ] })
252
+ ] }),
253
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
254
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Recent requests:" }),
255
+ /* @__PURE__ */ jsx4(RequestLog, { requests })
256
+ ] }),
257
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press Ctrl+C to stop" }) })
258
+ ] });
259
+ };
260
+
261
+ // src/tunnel/relay.ts
262
+ import { WebSocket } from "ws";
263
+ import { EventEmitter } from "events";
264
+
265
+ // src/config.ts
266
+ import { nanoid } from "nanoid";
267
+ import path from "path";
268
+ var RELAY_DOMAIN = "relay.untunneled.dev";
269
+ var SIGNALING_DOMAIN = "signal.untunneled.dev";
270
+ var TUNNEL_DOMAIN = "untunneled.dev";
271
+ var getRelayUrl = (tunnelId) => {
272
+ const host = process.env["UNTUNNELED_RELAY_HOST"] || RELAY_DOMAIN;
273
+ const protocol = host.includes("localhost") ? "ws" : "wss";
274
+ return `${protocol}://${host}/${tunnelId}`;
275
+ };
276
+ var getSignalingUrl = () => {
277
+ const host = process.env["UNTUNNELED_SIGNALING_HOST"] || SIGNALING_DOMAIN;
278
+ const protocol = host.includes("localhost") ? "ws" : "wss";
279
+ return `${protocol}://${host}`;
280
+ };
281
+ var getTunnelUrl = (tunnelId) => {
282
+ const domain = process.env["UNTUNNELED_TUNNEL_DOMAIN"] || TUNNEL_DOMAIN;
283
+ return `https://${tunnelId}.${domain}`;
284
+ };
285
+ function getProjectName() {
286
+ const cwd = process.cwd();
287
+ const folderName = path.basename(cwd);
288
+ return folderName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 20) || "tunnel";
289
+ }
290
+ function generateTunnelId(projectName) {
291
+ const name = projectName || getProjectName();
292
+ const suffix = nanoid(6).toLowerCase();
293
+ return `${name}-${suffix}`;
294
+ }
295
+ function validatePort(port) {
296
+ const portNum = typeof port === "string" ? parseInt(port, 10) : port;
297
+ if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
298
+ throw new Error(`Invalid port number: ${port}. Must be between 1 and 65535.`);
299
+ }
300
+ return portNum;
301
+ }
302
+ function parseAllowList(allow) {
303
+ if (!allow) return void 0;
304
+ return allow.split(",").map((email) => email.trim().toLowerCase()).filter((email) => email.length > 0);
305
+ }
306
+ var ICE_SERVERS = [
307
+ { urls: "stun:stun.l.google.com:19302" },
308
+ { urls: "stun:stun1.l.google.com:19302" },
309
+ { urls: "stun:stun2.l.google.com:19302" },
310
+ { urls: "stun:global.stun.twilio.com:3478" }
311
+ ];
312
+ var TIMEOUTS = {
313
+ P2P_CONNECTION: 1e4,
314
+ RELAY_REQUEST: 3e4,
315
+ WEBSOCKET_PING: 3e4,
316
+ RECONNECT_BASE: 1e3,
317
+ RECONNECT_MAX: 3e4
318
+ };
319
+
320
+ // src/tunnel/relay.ts
321
+ var RelayTunnel = class extends EventEmitter {
322
+ ws = null;
323
+ localPort;
324
+ tunnelId;
325
+ tunnelUrl;
326
+ options;
327
+ startTime = 0;
328
+ requestCount = 0;
329
+ reconnectAttempts = 0;
330
+ isClosing = false;
331
+ pingInterval = null;
332
+ constructor(options) {
333
+ super();
334
+ this.options = options;
335
+ this.localPort = options.localPort;
336
+ this.tunnelId = options.tunnelId;
337
+ this.tunnelUrl = options.tunnelUrl;
338
+ }
339
+ async connect() {
340
+ this.startTime = Date.now();
341
+ this.isClosing = false;
342
+ const wsUrl = getRelayUrl(this.tunnelId);
343
+ if (this.options.verbose) {
344
+ console.log(`[Relay] Connecting to ${wsUrl}`);
345
+ }
346
+ return new Promise((resolve, reject) => {
347
+ try {
348
+ this.ws = new WebSocket(wsUrl, {
349
+ headers: {
350
+ "X-Tunnel-Auth": this.options.password || "",
351
+ "X-Tunnel-Allow": this.options.allowList?.join(",") || "",
352
+ "X-Tunnel-Mode": this.options.mode || "relay"
353
+ }
354
+ });
355
+ const connectionTimeout = setTimeout(() => {
356
+ if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
357
+ this.ws.terminate();
358
+ reject(new Error("Connection timeout"));
359
+ }
360
+ }, TIMEOUTS.P2P_CONNECTION);
361
+ this.ws.on("open", () => {
362
+ clearTimeout(connectionTimeout);
363
+ this.reconnectAttempts = 0;
364
+ if (this.options.verbose) {
365
+ console.log("[Relay] Connected");
366
+ }
367
+ this.setupPing();
368
+ this.emit("connected");
369
+ resolve();
370
+ });
371
+ this.ws.on("error", (err) => {
372
+ clearTimeout(connectionTimeout);
373
+ if (this.options.verbose) {
374
+ console.error("[Relay] WebSocket error:", err.message);
375
+ }
376
+ this.emit("error", err);
377
+ reject(err);
378
+ });
379
+ this.ws.on("close", () => {
380
+ this.clearPing();
381
+ if (!this.isClosing) {
382
+ this.emit("disconnected");
383
+ this.attemptReconnect();
384
+ }
385
+ });
386
+ this.ws.on("message", async (data) => {
387
+ await this.handleRelayRequest(data);
388
+ });
389
+ } catch (error) {
390
+ reject(error);
391
+ }
392
+ });
393
+ }
394
+ async handleRelayRequest(data) {
395
+ const startTime = Date.now();
396
+ let request;
397
+ try {
398
+ request = JSON.parse(String(data));
399
+ } catch {
400
+ if (this.options.verbose) {
401
+ console.error("[Relay] Invalid request data");
402
+ }
403
+ return;
404
+ }
405
+ try {
406
+ const url = `http://localhost:${this.localPort}${request.path}`;
407
+ const fetchOptions = {
408
+ method: request.method,
409
+ headers: request.headers
410
+ };
411
+ if (request.body && !["GET", "HEAD"].includes(request.method)) {
412
+ fetchOptions.body = request.body;
413
+ }
414
+ const response = await fetch(url, fetchOptions);
415
+ const body = await response.text();
416
+ const duration = Date.now() - startTime;
417
+ const relayResponse = {
418
+ id: request.id,
419
+ status: response.status,
420
+ statusText: response.statusText,
421
+ headers: Object.fromEntries(response.headers.entries()),
422
+ body
423
+ };
424
+ this.ws?.send(JSON.stringify(relayResponse));
425
+ const log = {
426
+ id: request.id,
427
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().substring(11, 19),
428
+ method: request.method,
429
+ path: request.path,
430
+ status: response.status,
431
+ duration,
432
+ type: "relay"
433
+ };
434
+ this.emit("request", log);
435
+ this.requestCount++;
436
+ } catch (error) {
437
+ const err = error;
438
+ if (this.options.verbose) {
439
+ console.error(`[Relay] Proxy error: ${err.message}`);
440
+ }
441
+ const errorResponse = {
442
+ id: request.id,
443
+ status: 502,
444
+ statusText: "Bad Gateway",
445
+ headers: {},
446
+ body: `Error: ${err.message}`
447
+ };
448
+ this.ws?.send(JSON.stringify(errorResponse));
449
+ }
450
+ }
451
+ setupPing() {
452
+ this.pingInterval = setInterval(() => {
453
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
454
+ this.ws.ping();
455
+ }
456
+ }, TIMEOUTS.WEBSOCKET_PING);
457
+ }
458
+ clearPing() {
459
+ if (this.pingInterval) {
460
+ clearInterval(this.pingInterval);
461
+ this.pingInterval = null;
462
+ }
463
+ }
464
+ async attemptReconnect() {
465
+ if (this.isClosing) return;
466
+ this.reconnectAttempts++;
467
+ const delay = Math.min(
468
+ TIMEOUTS.RECONNECT_BASE * Math.pow(2, this.reconnectAttempts - 1),
469
+ TIMEOUTS.RECONNECT_MAX
470
+ );
471
+ if (this.options.verbose) {
472
+ console.log(`[Relay] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
473
+ }
474
+ setTimeout(async () => {
475
+ if (this.isClosing) return;
476
+ try {
477
+ await this.connect();
478
+ } catch (_) {
479
+ }
480
+ }, delay);
481
+ }
482
+ async disconnect() {
483
+ this.isClosing = true;
484
+ this.clearPing();
485
+ if (this.ws) {
486
+ this.ws.close(1e3, "Client closing");
487
+ this.ws = null;
488
+ }
489
+ }
490
+ getDuration() {
491
+ if (this.startTime === 0) return 0;
492
+ return Math.floor((Date.now() - this.startTime) / 1e3);
493
+ }
494
+ getRequestCount() {
495
+ return this.requestCount;
496
+ }
497
+ isConnected() {
498
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
499
+ }
500
+ getTunnelUrl() {
501
+ return this.tunnelUrl;
502
+ }
503
+ };
504
+
505
+ // src/tunnel/p2p.ts
506
+ import SimplePeer from "simple-peer";
507
+ import { WebSocket as WebSocket2 } from "ws";
508
+ import { EventEmitter as EventEmitter2 } from "events";
509
+ var P2PTunnel = class extends EventEmitter2 {
510
+ peer = null;
511
+ ws = null;
512
+ localPort;
513
+ tunnelId;
514
+ options;
515
+ connected = false;
516
+ requestCount = 0;
517
+ constructor(options) {
518
+ super();
519
+ this.options = options;
520
+ this.localPort = options.localPort;
521
+ this.tunnelId = options.tunnelId;
522
+ }
523
+ async connect() {
524
+ if (this.options.verbose) {
525
+ console.log("[P2P] Attempting connection...");
526
+ }
527
+ return new Promise((resolve, reject) => {
528
+ const signalingUrl = getSignalingUrl();
529
+ this.ws = new WebSocket2(signalingUrl);
530
+ const connectionTimeout = setTimeout(() => {
531
+ if (!this.connected) {
532
+ this.cleanup();
533
+ reject(new Error("P2P connection timeout"));
534
+ }
535
+ }, TIMEOUTS.P2P_CONNECTION);
536
+ this.ws.on("open", () => {
537
+ if (this.options.verbose) {
538
+ console.log("[P2P] Signaling connected");
539
+ }
540
+ const registerMsg = {
541
+ type: "register",
542
+ tunnelId: this.tunnelId,
543
+ role: "cli"
544
+ };
545
+ this.ws.send(JSON.stringify(registerMsg));
546
+ this.peer = new SimplePeer({
547
+ initiator: false,
548
+ trickle: false,
549
+ config: { iceServers: ICE_SERVERS }
550
+ });
551
+ this.peer.on("signal", (signal) => {
552
+ if (this.options.verbose) {
553
+ console.log("[P2P] Sending signal to browser");
554
+ }
555
+ const signalMsg = {
556
+ type: "signal",
557
+ from: "cli",
558
+ to: "browser",
559
+ tunnelId: this.tunnelId,
560
+ signal
561
+ };
562
+ this.ws.send(JSON.stringify(signalMsg));
563
+ });
564
+ this.peer.on("connect", () => {
565
+ clearTimeout(connectionTimeout);
566
+ this.connected = true;
567
+ this.emit("connected");
568
+ if (this.options.verbose) {
569
+ console.log("[P2P] Connection established");
570
+ }
571
+ resolve();
572
+ });
573
+ this.peer.on("data", async (data) => {
574
+ await this.handleP2PRequest(data);
575
+ });
576
+ this.peer.on("error", (err) => {
577
+ clearTimeout(connectionTimeout);
578
+ this.connected = false;
579
+ this.emit("error", err);
580
+ if (this.options.verbose) {
581
+ console.error("[P2P] Error:", err.message);
582
+ }
583
+ reject(err);
584
+ });
585
+ this.peer.on("close", () => {
586
+ this.connected = false;
587
+ this.emit("disconnected");
588
+ });
589
+ });
590
+ this.ws.on("message", (data) => {
591
+ const msg = JSON.parse(data.toString());
592
+ if (msg.type === "signal" && msg.from === "browser" && this.peer) {
593
+ if (this.options.verbose) {
594
+ console.log("[P2P] Received signal from browser");
595
+ }
596
+ this.peer.signal(msg.signal);
597
+ }
598
+ });
599
+ this.ws.on("error", (err) => {
600
+ clearTimeout(connectionTimeout);
601
+ reject(err);
602
+ });
603
+ });
604
+ }
605
+ async handleP2PRequest(data) {
606
+ const startTime = Date.now();
607
+ let request;
608
+ try {
609
+ request = JSON.parse(data.toString());
610
+ } catch {
611
+ return;
612
+ }
613
+ try {
614
+ const url = `http://localhost:${this.localPort}${request.path}`;
615
+ const fetchOptions = {
616
+ method: request.method,
617
+ headers: request.headers
618
+ };
619
+ if (request.body && !["GET", "HEAD"].includes(request.method)) {
620
+ fetchOptions.body = request.body;
621
+ }
622
+ const response = await fetch(url, fetchOptions);
623
+ const body = await response.text();
624
+ const duration = Date.now() - startTime;
625
+ const p2pResponse = {
626
+ id: request.id,
627
+ status: response.status,
628
+ statusText: response.statusText,
629
+ headers: Object.fromEntries(response.headers.entries()),
630
+ body
631
+ };
632
+ this.peer.send(JSON.stringify(p2pResponse));
633
+ const log = {
634
+ id: request.id,
635
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().substring(11, 19),
636
+ method: request.method,
637
+ path: request.path,
638
+ status: response.status,
639
+ duration,
640
+ type: "p2p"
641
+ };
642
+ this.emit("request", log);
643
+ this.requestCount++;
644
+ } catch (error) {
645
+ const err = error;
646
+ const errorResponse = {
647
+ id: request.id,
648
+ status: 502,
649
+ statusText: "Bad Gateway",
650
+ headers: {},
651
+ body: `Error: ${err.message}`
652
+ };
653
+ this.peer?.send(JSON.stringify(errorResponse));
654
+ }
655
+ }
656
+ cleanup() {
657
+ this.peer?.destroy();
658
+ this.ws?.close();
659
+ this.peer = null;
660
+ this.ws = null;
661
+ this.connected = false;
662
+ }
663
+ disconnect() {
664
+ this.cleanup();
665
+ }
666
+ isConnected() {
667
+ return this.connected;
668
+ }
669
+ getRequestCount() {
670
+ return this.requestCount;
671
+ }
672
+ };
673
+
674
+ // src/analytics.ts
675
+ import { PostHog } from "posthog-node";
676
+ var POSTHOG_API_KEY = process.env["POSTHOG_API_KEY"] || "phc_placeholder";
677
+ var POSTHOG_HOST = "https://app.posthog.com";
678
+ var Analytics = class {
679
+ posthog = null;
680
+ tunnelId;
681
+ enabled;
682
+ constructor(options) {
683
+ this.tunnelId = options.tunnelId;
684
+ this.enabled = options.enabled;
685
+ if (this.enabled && POSTHOG_API_KEY !== "phc_placeholder") {
686
+ this.posthog = new PostHog(POSTHOG_API_KEY, {
687
+ host: POSTHOG_HOST,
688
+ flushAt: 10,
689
+ flushInterval: 1e4
690
+ });
691
+ }
692
+ }
693
+ async track(event, properties) {
694
+ if (!this.posthog || !this.enabled) return;
695
+ try {
696
+ this.posthog.capture({
697
+ distinctId: this.tunnelId,
698
+ event,
699
+ properties: {
700
+ ...properties,
701
+ version: "0.1.0",
702
+ cli: true
703
+ }
704
+ });
705
+ } catch (_) {
706
+ }
707
+ }
708
+ async shutdown() {
709
+ if (!this.posthog) return;
710
+ try {
711
+ await this.posthog.shutdown();
712
+ } catch (_) {
713
+ }
714
+ }
715
+ isEnabled() {
716
+ return this.enabled && this.posthog !== null;
717
+ }
718
+ };
719
+
720
+ // src/commands/start.ts
721
+ async function startCommand(port, options) {
722
+ let localPort;
723
+ try {
724
+ localPort = validatePort(port);
725
+ } catch (error) {
726
+ console.error(error.message);
727
+ process.exit(1);
728
+ }
729
+ const projectName = getProjectName();
730
+ const tunnelId = generateTunnelId(projectName);
731
+ const tunnelUrl = getTunnelUrl(tunnelId);
732
+ const mode = options.relayFallback ? "p2p-with-fallback" : "p2p-only";
733
+ const analytics = new Analytics({
734
+ enabled: options.telemetry !== false,
735
+ tunnelId
736
+ });
737
+ await analytics.track("tunnel_start", {
738
+ mode,
739
+ has_auth: !!options.password || !!options.allow,
740
+ node_version: process.version,
741
+ platform: process.platform
742
+ });
743
+ const relayTunnel = new RelayTunnel({
744
+ localPort,
745
+ tunnelId,
746
+ tunnelUrl,
747
+ password: options.password,
748
+ allowList: parseAllowList(options.allow),
749
+ verbose: options.verbose,
750
+ mode
751
+ });
752
+ let p2pTunnel = null;
753
+ let p2pError = null;
754
+ try {
755
+ await relayTunnel.connect();
756
+ if (options.verbose) {
757
+ console.log(`Tunnel URL: ${tunnelUrl}`);
758
+ console.log(`Forwarding to localhost:${localPort}`);
759
+ }
760
+ } catch (error) {
761
+ console.error("Failed to connect to relay server:", error.message);
762
+ await analytics.shutdown();
763
+ process.exit(1);
764
+ }
765
+ p2pTunnel = new P2PTunnel({
766
+ localPort,
767
+ tunnelId,
768
+ verbose: options.verbose
769
+ });
770
+ try {
771
+ await p2pTunnel.connect();
772
+ await analytics.track("p2p_success", { mode });
773
+ } catch (error) {
774
+ p2pError = error;
775
+ await analytics.track("p2p_failed", { mode, reason: p2pError.message });
776
+ if (!options.relayFallback) {
777
+ console.error("");
778
+ console.error("P2P connection failed:", p2pError.message);
779
+ console.error("");
780
+ console.error("Subsequent requests from the browser will fail.");
781
+ console.error("");
782
+ console.error("Options:");
783
+ console.error(" 1. Use --relay-fallback for automatic fallback");
784
+ console.error(" 2. Try a different network (mobile hotspot, home WiFi)");
785
+ console.error("");
786
+ }
787
+ }
788
+ const { waitUntilExit } = render(
789
+ React5.createElement(TunnelUI, {
790
+ tunnelUrl,
791
+ localPort,
792
+ showQR: options.qr,
793
+ mode,
794
+ relayTunnel,
795
+ p2pTunnel: p2pTunnel?.isConnected() ? p2pTunnel : null,
796
+ p2pError
797
+ })
798
+ );
799
+ const cleanup = async () => {
800
+ const duration = relayTunnel.getDuration();
801
+ const relayRequests = relayTunnel.getRequestCount();
802
+ const p2pRequests = p2pTunnel?.getRequestCount() ?? 0;
803
+ await analytics.track("tunnel_end", {
804
+ duration_seconds: duration,
805
+ requests_relay: relayRequests,
806
+ requests_p2p: p2pRequests,
807
+ p2p_success: p2pTunnel?.isConnected() ?? false
808
+ });
809
+ await relayTunnel.disconnect();
810
+ p2pTunnel?.disconnect();
811
+ await analytics.shutdown();
812
+ };
813
+ process.on("SIGINT", async () => {
814
+ await cleanup();
815
+ process.exit(0);
816
+ });
817
+ process.on("SIGTERM", async () => {
818
+ await cleanup();
819
+ process.exit(0);
820
+ });
821
+ await waitUntilExit();
822
+ }
823
+
824
+ // src/commands/list.ts
825
+ async function listCommand() {
826
+ console.log("Active tunnels:");
827
+ console.log(" (No active tunnels)");
828
+ console.log("");
829
+ console.log("Note: Tunnel list feature coming soon.");
830
+ }
831
+
832
+ // src/commands/stop.ts
833
+ async function stopCommand(tunnelId) {
834
+ console.log(`Stopping tunnel: ${tunnelId}`);
835
+ console.log("");
836
+ console.log("Note: Remote stop feature coming soon.");
837
+ }
838
+
839
+ // src/index.ts
840
+ var program = new Command();
841
+ program.name("untunneled").description("Privacy-first localhost tunneling - P2P by default").version("0.1.0");
842
+ program.argument("<port>", "Local port to tunnel").option("-r, --relay-fallback", "Enable HTTP relay fallback if P2P fails").option("--qr", "Show QR code for mobile access").option("--password <password>", "Password protect the tunnel").option("--allow <emails>", "Email whitelist (comma-separated)").option("--team <name>", "Team tunnel name").option("--verbose", "Show detailed logs").option("--no-telemetry", "Disable anonymous usage tracking").action(startCommand);
843
+ program.command("list").description("List active tunnels").action(listCommand);
844
+ program.command("stop").argument("<tunnel-id>", "Tunnel ID to stop").description("Stop a running tunnel").action(stopCommand);
845
+ program.parse();
846
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/commands/start.ts","../src/ui/TunnelUI.tsx","../src/ui/QRCode.tsx","../src/ui/RequestLog.tsx","../src/ui/ConnectionStatus.tsx","../src/tunnel/relay.ts","../src/config.ts","../src/tunnel/p2p.ts","../src/analytics.ts","../src/commands/list.ts","../src/commands/stop.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { startCommand, listCommand, stopCommand } from './commands/index.js';\n\nconst program = new Command();\n\nprogram\n .name('untunneled')\n .description('Privacy-first localhost tunneling - P2P by default')\n .version('0.1.0');\n\nprogram\n .argument('<port>', 'Local port to tunnel')\n .option('-r, --relay-fallback', 'Enable HTTP relay fallback if P2P fails')\n .option('--qr', 'Show QR code for mobile access')\n .option('--password <password>', 'Password protect the tunnel')\n .option('--allow <emails>', 'Email whitelist (comma-separated)')\n .option('--team <name>', 'Team tunnel name')\n .option('--verbose', 'Show detailed logs')\n .option('--no-telemetry', 'Disable anonymous usage tracking')\n .action(startCommand);\n\nprogram.command('list').description('List active tunnels').action(listCommand);\n\nprogram\n .command('stop')\n .argument('<tunnel-id>', 'Tunnel ID to stop')\n .description('Stop a running tunnel')\n .action(stopCommand);\n\nprogram.parse();\n","import { render } from 'ink';\nimport React from 'react';\nimport { TunnelUI } from '../ui/TunnelUI.js';\nimport { RelayTunnel } from '../tunnel/relay.js';\nimport { P2PTunnel } from '../tunnel/p2p.js';\nimport { Analytics } from '../analytics.js';\nimport {\n generateTunnelId,\n getProjectName,\n getTunnelUrl,\n validatePort,\n parseAllowList,\n} from '../config.js';\nimport type { StartOptions, TunnelMode } from '../types.js';\n\nexport async function startCommand(port: string, options: StartOptions): Promise<void> {\n let localPort: number;\n\n try {\n localPort = validatePort(port);\n } catch (error) {\n console.error((error as Error).message);\n process.exit(1);\n }\n\n const projectName = getProjectName();\n const tunnelId = generateTunnelId(projectName);\n const tunnelUrl = getTunnelUrl(tunnelId);\n const mode: TunnelMode = options.relayFallback ? 'p2p-with-fallback' : 'p2p-only';\n\n const analytics = new Analytics({\n enabled: options.telemetry !== false,\n tunnelId,\n });\n\n await analytics.track('tunnel_start', {\n mode,\n has_auth: !!options.password || !!options.allow,\n node_version: process.version,\n platform: process.platform,\n });\n\n const relayTunnel = new RelayTunnel({\n localPort,\n tunnelId,\n tunnelUrl,\n password: options.password,\n allowList: parseAllowList(options.allow),\n verbose: options.verbose,\n mode,\n });\n\n let p2pTunnel: P2PTunnel | null = null;\n let p2pError: Error | null = null;\n\n try {\n await relayTunnel.connect();\n\n if (options.verbose) {\n console.log(`Tunnel URL: ${tunnelUrl}`);\n console.log(`Forwarding to localhost:${localPort}`);\n }\n } catch (error) {\n console.error('Failed to connect to relay server:', (error as Error).message);\n await analytics.shutdown();\n process.exit(1);\n }\n\n p2pTunnel = new P2PTunnel({\n localPort,\n tunnelId,\n verbose: options.verbose,\n });\n\n try {\n await p2pTunnel.connect();\n await analytics.track('p2p_success', { mode });\n } catch (error) {\n p2pError = error as Error;\n await analytics.track('p2p_failed', { mode, reason: p2pError.message });\n\n if (!options.relayFallback) {\n console.error('');\n console.error('P2P connection failed:', p2pError.message);\n console.error('');\n console.error('Subsequent requests from the browser will fail.');\n console.error('');\n console.error('Options:');\n console.error(' 1. Use --relay-fallback for automatic fallback');\n console.error(' 2. Try a different network (mobile hotspot, home WiFi)');\n console.error('');\n }\n }\n\n const { waitUntilExit } = render(\n React.createElement(TunnelUI, {\n tunnelUrl,\n localPort,\n showQR: options.qr,\n mode,\n relayTunnel,\n p2pTunnel: p2pTunnel?.isConnected() ? p2pTunnel : null,\n p2pError,\n })\n );\n\n const cleanup = async () => {\n const duration = relayTunnel.getDuration();\n const relayRequests = relayTunnel.getRequestCount();\n const p2pRequests = p2pTunnel?.getRequestCount() ?? 0;\n\n await analytics.track('tunnel_end', {\n duration_seconds: duration,\n requests_relay: relayRequests,\n requests_p2p: p2pRequests,\n p2p_success: p2pTunnel?.isConnected() ?? false,\n });\n\n await relayTunnel.disconnect();\n p2pTunnel?.disconnect();\n await analytics.shutdown();\n };\n\n process.on('SIGINT', async () => {\n await cleanup();\n process.exit(0);\n });\n\n process.on('SIGTERM', async () => {\n await cleanup();\n process.exit(0);\n });\n\n await waitUntilExit();\n}\n","import React, { useState, useEffect } from 'react';\nimport { Box, Text, Newline, useApp } from 'ink';\nimport { QRCode } from './QRCode.js';\nimport { RequestLog } from './RequestLog.js';\nimport { ConnectionStatus } from './ConnectionStatus.js';\nimport type { RelayTunnel } from '../tunnel/relay.js';\nimport type { P2PTunnel } from '../tunnel/p2p.js';\nimport type { RequestLog as RequestLogType, TunnelMode, TunnelStats } from '../types.js';\n\ninterface TunnelUIProps {\n tunnelUrl: string;\n localPort: number;\n showQR?: boolean;\n mode: TunnelMode;\n relayTunnel: RelayTunnel;\n p2pTunnel?: P2PTunnel | null;\n p2pError?: Error | null;\n}\n\nconst MAX_LOGS = 10;\n\nexport const TunnelUI: React.FC<TunnelUIProps> = ({\n tunnelUrl,\n localPort,\n showQR,\n mode,\n relayTunnel,\n p2pTunnel,\n p2pError,\n}) => {\n useApp();\n const [requests, setRequests] = useState<RequestLogType[]>([]);\n const [stats, setStats] = useState<TunnelStats>({\n total: 0,\n p2p: 0,\n relay: 0,\n startTime: Date.now(),\n });\n const [relayConnected, setRelayConnected] = useState(relayTunnel.isConnected());\n const [p2pConnected, setP2pConnected] = useState(p2pTunnel?.isConnected() ?? false);\n\n useEffect(() => {\n const handleRequest = (req: RequestLogType) => {\n setRequests((prev) => [...prev.slice(-(MAX_LOGS - 1)), req]);\n setStats((prev) => ({\n ...prev,\n total: prev.total + 1,\n p2p: prev.p2p + (req.type === 'p2p' ? 1 : 0),\n relay: prev.relay + (req.type === 'relay' ? 1 : 0),\n }));\n };\n\n const handleRelayConnected = () => setRelayConnected(true);\n const handleRelayDisconnected = () => setRelayConnected(false);\n const handleP2PConnected = () => setP2pConnected(true);\n const handleP2PDisconnected = () => setP2pConnected(false);\n\n relayTunnel.on('request', handleRequest);\n relayTunnel.on('connected', handleRelayConnected);\n relayTunnel.on('disconnected', handleRelayDisconnected);\n\n if (p2pTunnel) {\n p2pTunnel.on('request', handleRequest);\n p2pTunnel.on('connected', handleP2PConnected);\n p2pTunnel.on('disconnected', handleP2PDisconnected);\n }\n\n return () => {\n relayTunnel.off('request', handleRequest);\n relayTunnel.off('connected', handleRelayConnected);\n relayTunnel.off('disconnected', handleRelayDisconnected);\n\n if (p2pTunnel) {\n p2pTunnel.off('request', handleRequest);\n p2pTunnel.off('connected', handleP2PConnected);\n p2pTunnel.off('disconnected', handleP2PDisconnected);\n }\n };\n }, [relayTunnel, p2pTunnel]);\n\n const connectionStatus = relayConnected || p2pConnected ? 'connected' : 'connecting';\n const borderColor = p2pConnected ? 'green' : relayConnected ? 'yellow' : 'gray';\n\n return (\n <Box flexDirection=\"column\" padding={1}>\n <Box borderStyle=\"round\" borderColor={borderColor} padding={1} flexDirection=\"column\">\n <Text bold color={borderColor}>\n {p2pConnected ? '🔒' : '⚡'} untunneled.dev\n </Text>\n <Newline />\n <ConnectionStatus\n status={connectionStatus}\n mode={mode}\n p2pConnected={p2pConnected}\n relayConnected={relayConnected}\n />\n <Newline />\n <Box>\n <Text>URL: </Text>\n <Text bold color=\"cyan\">\n {tunnelUrl}\n </Text>\n </Box>\n <Box>\n <Text dimColor>→ localhost:{localPort}</Text>\n </Box>\n </Box>\n\n {p2pError && mode === 'p2p-only' && (\n <Box\n marginTop={1}\n borderStyle=\"single\"\n borderColor=\"red\"\n padding={1}\n flexDirection=\"column\"\n >\n <Text color=\"red\" bold>\n P2P Connection Failed\n </Text>\n <Text dimColor>{p2pError.message}</Text>\n <Newline />\n <Text dimColor>Use --relay-fallback for guaranteed compatibility</Text>\n </Box>\n )}\n\n {showQR && (\n <Box marginTop={1}>\n <QRCode url={tunnelUrl} />\n </Box>\n )}\n\n <Box marginTop={1} gap={2}>\n <Text>\n Requests: <Text color=\"cyan\">{stats.total}</Text>\n </Text>\n {mode !== 'relay' && stats.total > 0 && (\n <>\n <Text>\n P2P: <Text color=\"green\">{stats.p2p}</Text>\n </Text>\n <Text>\n Relay: <Text color=\"yellow\">{stats.relay}</Text>\n </Text>\n <Text dimColor>({Math.round((stats.p2p / stats.total) * 100)}% private)</Text>\n </>\n )}\n </Box>\n\n <Box marginTop={1} flexDirection=\"column\">\n <Text dimColor>Recent requests:</Text>\n <RequestLog requests={requests} />\n </Box>\n\n <Box marginTop={1}>\n <Text dimColor>Press Ctrl+C to stop</Text>\n </Box>\n </Box>\n );\n};\n","import React, { useState, useEffect } from 'react';\nimport { Box, Text } from 'ink';\nimport qrcode from 'qrcode-terminal';\n\ninterface QRCodeProps {\n url: string;\n}\n\nexport const QRCode: React.FC<QRCodeProps> = ({ url }) => {\n const [qrString, setQrString] = useState('');\n\n useEffect(() => {\n qrcode.generate(url, { small: true }, (qr: string) => {\n setQrString(qr);\n });\n }, [url]);\n\n if (!qrString) {\n return (\n <Box>\n <Text dimColor>Generating QR code...</Text>\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\" borderStyle=\"single\" paddingX={1}>\n <Text dimColor>Scan with mobile:</Text>\n <Text>{qrString}</Text>\n </Box>\n );\n};\n","import React from 'react';\nimport { Box, Text } from 'ink';\nimport type { RequestLog as RequestLogType } from '../types.js';\n\ninterface RequestLogProps {\n requests: RequestLogType[];\n}\n\nconst getStatusColor = (status: number): string => {\n if (status >= 500) return 'red';\n if (status >= 400) return 'yellow';\n if (status >= 300) return 'blue';\n return 'green';\n};\n\nconst getMethodColor = (method: string): string => {\n switch (method) {\n case 'GET':\n return 'green';\n case 'POST':\n return 'yellow';\n case 'PUT':\n return 'blue';\n case 'DELETE':\n return 'red';\n case 'PATCH':\n return 'magenta';\n default:\n return 'white';\n }\n};\n\nexport const RequestLog: React.FC<RequestLogProps> = ({ requests }) => {\n if (requests.length === 0) {\n return <Text dimColor> Waiting for requests...</Text>;\n }\n\n return (\n <Box flexDirection=\"column\">\n {requests.map((req) => (\n <Box key={req.id} gap={1}>\n <Text dimColor>{req.timestamp}</Text>\n <Box width={7}>\n <Text color={getMethodColor(req.method)}>{req.method.padEnd(6)}</Text>\n </Box>\n <Box flexGrow={1}>\n <Text>{req.path.length > 40 ? req.path.substring(0, 37) + '...' : req.path}</Text>\n </Box>\n <Box width={5}>\n <Text color={getStatusColor(req.status)}>{req.status}</Text>\n </Box>\n <Box width={8}>\n <Text dimColor>{req.duration}ms</Text>\n </Box>\n <Box width={7}>\n <Text color={req.type === 'p2p' ? 'green' : 'yellow'}>[{req.type.toUpperCase()}]</Text>\n </Box>\n </Box>\n ))}\n </Box>\n );\n};\n","import React from 'react';\nimport { Box, Text } from 'ink';\nimport type { ConnectionStatus as ConnectionStatusType, TunnelMode } from '../types.js';\n\ninterface ConnectionStatusProps {\n status: ConnectionStatusType;\n mode: TunnelMode;\n p2pConnected: boolean;\n relayConnected: boolean;\n}\n\nconst getStatusIndicator = (status: ConnectionStatusType): { color: string; symbol: string } => {\n switch (status) {\n case 'connected':\n return { color: 'green', symbol: '●' };\n case 'connecting':\n return { color: 'yellow', symbol: '◐' };\n case 'disconnected':\n return { color: 'red', symbol: '○' };\n case 'error':\n return { color: 'red', symbol: '✗' };\n }\n};\n\nconst getModeLabel = (mode: TunnelMode, p2pConnected: boolean): string => {\n if (mode === 'relay') return 'HTTP Relay';\n if (mode === 'p2p-only') return p2pConnected ? 'P2P Direct' : 'P2P (connecting...)';\n return p2pConnected ? 'P2P + Relay Fallback' : 'Relay (P2P pending...)';\n};\n\nexport const ConnectionStatus: React.FC<ConnectionStatusProps> = ({\n status,\n mode,\n p2pConnected,\n relayConnected,\n}) => {\n const indicator = getStatusIndicator(status);\n const modeLabel = getModeLabel(mode, p2pConnected);\n\n return (\n <Box gap={2}>\n <Box>\n <Text color={indicator.color}>{indicator.symbol}</Text>\n <Text> </Text>\n <Text color={indicator.color}>{status.toUpperCase()}</Text>\n </Box>\n <Box>\n <Text dimColor>Mode: </Text>\n <Text color={p2pConnected ? 'green' : 'yellow'}>{modeLabel}</Text>\n </Box>\n {mode !== 'relay' && (\n <Box>\n <Text dimColor>P2P: </Text>\n <Text color={p2pConnected ? 'green' : 'yellow'}>\n {p2pConnected ? 'Active' : 'Pending'}\n </Text>\n </Box>\n )}\n <Box>\n <Text dimColor>Relay: </Text>\n <Text color={relayConnected ? 'green' : 'red'}>\n {relayConnected ? 'Connected' : 'Disconnected'}\n </Text>\n </Box>\n </Box>\n );\n};\n","import { WebSocket } from 'ws';\nimport { EventEmitter } from 'node:events';\nimport { getRelayUrl, TIMEOUTS } from '../config.js';\nimport type { RelayRequest, RelayResponse, RequestLog, TunnelMode } from '../types.js';\n\ninterface RelayTunnelOptions {\n localPort: number;\n tunnelId: string;\n tunnelUrl: string;\n password?: string;\n allowList?: string[];\n verbose?: boolean;\n mode?: TunnelMode;\n}\n\nexport class RelayTunnel extends EventEmitter {\n private ws: WebSocket | null = null;\n private localPort: number;\n private tunnelId: string;\n private tunnelUrl: string;\n private options: RelayTunnelOptions;\n private startTime: number = 0;\n private requestCount: number = 0;\n private reconnectAttempts: number = 0;\n private isClosing: boolean = false;\n private pingInterval: NodeJS.Timeout | null = null;\n\n constructor(options: RelayTunnelOptions) {\n super();\n this.options = options;\n this.localPort = options.localPort;\n this.tunnelId = options.tunnelId;\n this.tunnelUrl = options.tunnelUrl;\n }\n\n async connect(): Promise<void> {\n this.startTime = Date.now();\n this.isClosing = false;\n\n const wsUrl = getRelayUrl(this.tunnelId);\n\n if (this.options.verbose) {\n console.log(`[Relay] Connecting to ${wsUrl}`);\n }\n\n return new Promise((resolve, reject) => {\n try {\n this.ws = new WebSocket(wsUrl, {\n headers: {\n 'X-Tunnel-Auth': this.options.password || '',\n 'X-Tunnel-Allow': this.options.allowList?.join(',') || '',\n 'X-Tunnel-Mode': this.options.mode || 'relay',\n },\n });\n\n const connectionTimeout = setTimeout(() => {\n if (this.ws && this.ws.readyState !== WebSocket.OPEN) {\n this.ws.terminate();\n reject(new Error('Connection timeout'));\n }\n }, TIMEOUTS.P2P_CONNECTION);\n\n this.ws.on('open', () => {\n clearTimeout(connectionTimeout);\n this.reconnectAttempts = 0;\n\n if (this.options.verbose) {\n console.log('[Relay] Connected');\n }\n\n this.setupPing();\n this.emit('connected');\n resolve();\n });\n\n this.ws.on('error', (err) => {\n clearTimeout(connectionTimeout);\n\n if (this.options.verbose) {\n console.error('[Relay] WebSocket error:', err.message);\n }\n\n this.emit('error', err);\n reject(err);\n });\n\n this.ws.on('close', () => {\n this.clearPing();\n\n if (!this.isClosing) {\n this.emit('disconnected');\n this.attemptReconnect();\n }\n });\n\n this.ws.on('message', async (data) => {\n await this.handleRelayRequest(data);\n });\n } catch (error) {\n reject(error);\n }\n });\n }\n\n private async handleRelayRequest(data: Buffer | ArrayBuffer | Buffer[]): Promise<void> {\n const startTime = Date.now();\n let request: RelayRequest;\n\n try {\n request = JSON.parse(String(data)) as RelayRequest;\n } catch {\n if (this.options.verbose) {\n console.error('[Relay] Invalid request data');\n }\n return;\n }\n\n try {\n const url = `http://localhost:${this.localPort}${request.path}`;\n\n const fetchOptions: RequestInit = {\n method: request.method,\n headers: request.headers,\n };\n\n if (request.body && !['GET', 'HEAD'].includes(request.method)) {\n fetchOptions.body = request.body;\n }\n\n const response = await fetch(url, fetchOptions);\n const body = await response.text();\n const duration = Date.now() - startTime;\n\n const relayResponse: RelayResponse = {\n id: request.id,\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body,\n };\n\n this.ws?.send(JSON.stringify(relayResponse));\n\n const log: RequestLog = {\n id: request.id,\n timestamp: new Date().toISOString().substring(11, 19),\n method: request.method,\n path: request.path,\n status: response.status,\n duration,\n type: 'relay',\n };\n\n this.emit('request', log);\n this.requestCount++;\n } catch (error) {\n const err = error as Error;\n\n if (this.options.verbose) {\n console.error(`[Relay] Proxy error: ${err.message}`);\n }\n\n const errorResponse: RelayResponse = {\n id: request.id,\n status: 502,\n statusText: 'Bad Gateway',\n headers: {},\n body: `Error: ${err.message}`,\n };\n\n this.ws?.send(JSON.stringify(errorResponse));\n }\n }\n\n private setupPing(): void {\n this.pingInterval = setInterval(() => {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.ping();\n }\n }, TIMEOUTS.WEBSOCKET_PING);\n }\n\n private clearPing(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n private async attemptReconnect(): Promise<void> {\n if (this.isClosing) return;\n\n this.reconnectAttempts++;\n const delay = Math.min(\n TIMEOUTS.RECONNECT_BASE * Math.pow(2, this.reconnectAttempts - 1),\n TIMEOUTS.RECONNECT_MAX\n );\n\n if (this.options.verbose) {\n console.log(`[Relay] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);\n }\n\n setTimeout(async () => {\n if (this.isClosing) return;\n\n try {\n await this.connect();\n } catch (_) {}\n }, delay);\n }\n\n async disconnect(): Promise<void> {\n this.isClosing = true;\n this.clearPing();\n\n if (this.ws) {\n this.ws.close(1000, 'Client closing');\n this.ws = null;\n }\n }\n\n getDuration(): number {\n if (this.startTime === 0) return 0;\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n getRequestCount(): number {\n return this.requestCount;\n }\n\n isConnected(): boolean {\n return this.ws !== null && this.ws.readyState === WebSocket.OPEN;\n }\n\n getTunnelUrl(): string {\n return this.tunnelUrl;\n }\n}\n","import { nanoid } from 'nanoid';\nimport path from 'node:path';\n\nexport const RELAY_DOMAIN = 'relay.untunneled.dev';\nexport const SIGNALING_DOMAIN = 'signal.untunneled.dev';\nexport const TUNNEL_DOMAIN = 'untunneled.dev';\n\nexport const getRelayUrl = (tunnelId: string): string => {\n const host = process.env['UNTUNNELED_RELAY_HOST'] || RELAY_DOMAIN;\n const protocol = host.includes('localhost') ? 'ws' : 'wss';\n return `${protocol}://${host}/${tunnelId}`;\n};\n\nexport const getSignalingUrl = (): string => {\n const host = process.env['UNTUNNELED_SIGNALING_HOST'] || SIGNALING_DOMAIN;\n const protocol = host.includes('localhost') ? 'ws' : 'wss';\n return `${protocol}://${host}`;\n};\n\nexport const getTunnelUrl = (tunnelId: string): string => {\n const domain = process.env['UNTUNNELED_TUNNEL_DOMAIN'] || TUNNEL_DOMAIN;\n return `https://${tunnelId}.${domain}`;\n};\n\nexport function getProjectName(): string {\n const cwd = process.cwd();\n const folderName = path.basename(cwd);\n\n return (\n folderName\n .toLowerCase()\n .replace(/[^a-z0-9-]/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 20) || 'tunnel'\n );\n}\n\nexport function generateTunnelId(projectName?: string): string {\n const name = projectName || getProjectName();\n const suffix = nanoid(6).toLowerCase();\n return `${name}-${suffix}`;\n}\n\nexport function validatePort(port: string | number): number {\n const portNum = typeof port === 'string' ? parseInt(port, 10) : port;\n\n if (isNaN(portNum) || portNum < 1 || portNum > 65535) {\n throw new Error(`Invalid port number: ${port}. Must be between 1 and 65535.`);\n }\n\n return portNum;\n}\n\nexport function parseAllowList(allow?: string): string[] | undefined {\n if (!allow) return undefined;\n\n return allow\n .split(',')\n .map((email) => email.trim().toLowerCase())\n .filter((email) => email.length > 0);\n}\n\nexport const ICE_SERVERS = [\n { urls: 'stun:stun.l.google.com:19302' },\n { urls: 'stun:stun1.l.google.com:19302' },\n { urls: 'stun:stun2.l.google.com:19302' },\n { urls: 'stun:global.stun.twilio.com:3478' },\n];\n\nexport const TIMEOUTS = {\n P2P_CONNECTION: 10000,\n RELAY_REQUEST: 30000,\n WEBSOCKET_PING: 30000,\n RECONNECT_BASE: 1000,\n RECONNECT_MAX: 30000,\n};\n","import SimplePeer from 'simple-peer';\nimport { WebSocket } from 'ws';\nimport { EventEmitter } from 'node:events';\nimport { getSignalingUrl, ICE_SERVERS, TIMEOUTS } from '../config.js';\nimport type { RelayRequest, RelayResponse, RequestLog, SignalingMessage } from '../types.js';\n\ninterface P2PTunnelOptions {\n localPort: number;\n tunnelId: string;\n verbose?: boolean;\n}\n\nexport class P2PTunnel extends EventEmitter {\n private peer: SimplePeer.Instance | null = null;\n private ws: WebSocket | null = null;\n private localPort: number;\n private tunnelId: string;\n private options: P2PTunnelOptions;\n private connected: boolean = false;\n private requestCount: number = 0;\n\n constructor(options: P2PTunnelOptions) {\n super();\n this.options = options;\n this.localPort = options.localPort;\n this.tunnelId = options.tunnelId;\n }\n\n async connect(): Promise<void> {\n if (this.options.verbose) {\n console.log('[P2P] Attempting connection...');\n }\n\n return new Promise((resolve, reject) => {\n const signalingUrl = getSignalingUrl();\n this.ws = new WebSocket(signalingUrl);\n\n const connectionTimeout = setTimeout(() => {\n if (!this.connected) {\n this.cleanup();\n reject(new Error('P2P connection timeout'));\n }\n }, TIMEOUTS.P2P_CONNECTION);\n\n this.ws.on('open', () => {\n if (this.options.verbose) {\n console.log('[P2P] Signaling connected');\n }\n\n const registerMsg: SignalingMessage = {\n type: 'register',\n tunnelId: this.tunnelId,\n role: 'cli',\n };\n this.ws!.send(JSON.stringify(registerMsg));\n\n this.peer = new SimplePeer({\n initiator: false,\n trickle: false,\n config: { iceServers: ICE_SERVERS },\n });\n\n this.peer.on('signal', (signal) => {\n if (this.options.verbose) {\n console.log('[P2P] Sending signal to browser');\n }\n\n const signalMsg: SignalingMessage = {\n type: 'signal',\n from: 'cli',\n to: 'browser',\n tunnelId: this.tunnelId,\n signal,\n };\n this.ws!.send(JSON.stringify(signalMsg));\n });\n\n this.peer.on('connect', () => {\n clearTimeout(connectionTimeout);\n this.connected = true;\n this.emit('connected');\n\n if (this.options.verbose) {\n console.log('[P2P] Connection established');\n }\n\n resolve();\n });\n\n this.peer.on('data', async (data) => {\n await this.handleP2PRequest(data);\n });\n\n this.peer.on('error', (err) => {\n clearTimeout(connectionTimeout);\n this.connected = false;\n this.emit('error', err);\n\n if (this.options.verbose) {\n console.error('[P2P] Error:', err.message);\n }\n\n reject(err);\n });\n\n this.peer.on('close', () => {\n this.connected = false;\n this.emit('disconnected');\n });\n });\n\n this.ws.on('message', (data) => {\n const msg = JSON.parse(data.toString()) as SignalingMessage;\n\n if (msg.type === 'signal' && msg.from === 'browser' && this.peer) {\n if (this.options.verbose) {\n console.log('[P2P] Received signal from browser');\n }\n this.peer.signal(msg.signal as SimplePeer.SignalData);\n }\n });\n\n this.ws.on('error', (err) => {\n clearTimeout(connectionTimeout);\n reject(err);\n });\n });\n }\n\n private async handleP2PRequest(data: Uint8Array): Promise<void> {\n const startTime = Date.now();\n let request: RelayRequest;\n\n try {\n request = JSON.parse(data.toString()) as RelayRequest;\n } catch {\n return;\n }\n\n try {\n const url = `http://localhost:${this.localPort}${request.path}`;\n\n const fetchOptions: RequestInit = {\n method: request.method,\n headers: request.headers,\n };\n\n if (request.body && !['GET', 'HEAD'].includes(request.method)) {\n fetchOptions.body = request.body;\n }\n\n const response = await fetch(url, fetchOptions);\n const body = await response.text();\n const duration = Date.now() - startTime;\n\n const p2pResponse: RelayResponse = {\n id: request.id,\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body,\n };\n\n this.peer!.send(JSON.stringify(p2pResponse));\n\n const log: RequestLog = {\n id: request.id,\n timestamp: new Date().toISOString().substring(11, 19),\n method: request.method,\n path: request.path,\n status: response.status,\n duration,\n type: 'p2p',\n };\n\n this.emit('request', log);\n this.requestCount++;\n } catch (error) {\n const err = error as Error;\n\n const errorResponse: RelayResponse = {\n id: request.id,\n status: 502,\n statusText: 'Bad Gateway',\n headers: {},\n body: `Error: ${err.message}`,\n };\n\n this.peer?.send(JSON.stringify(errorResponse));\n }\n }\n\n private cleanup(): void {\n this.peer?.destroy();\n this.ws?.close();\n this.peer = null;\n this.ws = null;\n this.connected = false;\n }\n\n disconnect(): void {\n this.cleanup();\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n getRequestCount(): number {\n return this.requestCount;\n }\n}\n","import { PostHog } from 'posthog-node';\n\nconst POSTHOG_API_KEY = process.env['POSTHOG_API_KEY'] || 'phc_placeholder';\nconst POSTHOG_HOST = 'https://app.posthog.com';\n\ninterface AnalyticsOptions {\n enabled: boolean;\n tunnelId: string;\n}\n\ntype EventName = 'tunnel_start' | 'tunnel_end' | 'p2p_success' | 'p2p_failed' | 'request_handled';\n\ninterface EventProperties {\n tunnel_start: {\n mode: string;\n has_auth: boolean;\n node_version: string;\n platform: string;\n };\n tunnel_end: {\n duration_seconds: number;\n requests_relay: number;\n requests_p2p: number;\n p2p_success: boolean;\n };\n p2p_success: {\n mode: string;\n connection_time_ms?: number;\n };\n p2p_failed: {\n mode: string;\n reason: string;\n };\n request_handled: {\n transport: 'p2p' | 'relay';\n method: string;\n status: number;\n duration_ms: number;\n };\n}\n\nexport class Analytics {\n private posthog: PostHog | null = null;\n private tunnelId: string;\n private enabled: boolean;\n\n constructor(options: AnalyticsOptions) {\n this.tunnelId = options.tunnelId;\n this.enabled = options.enabled;\n\n if (this.enabled && POSTHOG_API_KEY !== 'phc_placeholder') {\n this.posthog = new PostHog(POSTHOG_API_KEY, {\n host: POSTHOG_HOST,\n flushAt: 10,\n flushInterval: 10000,\n });\n }\n }\n\n async track<E extends EventName>(event: E, properties?: EventProperties[E]): Promise<void> {\n if (!this.posthog || !this.enabled) return;\n\n try {\n this.posthog.capture({\n distinctId: this.tunnelId,\n event,\n properties: {\n ...properties,\n version: '0.1.0',\n cli: true,\n },\n });\n } catch (_) {}\n }\n\n async shutdown(): Promise<void> {\n if (!this.posthog) return;\n\n try {\n await this.posthog.shutdown();\n } catch (_) {}\n }\n\n isEnabled(): boolean {\n return this.enabled && this.posthog !== null;\n }\n}\n","export async function listCommand(): Promise<void> {\n console.log('Active tunnels:');\n console.log(' (No active tunnels)');\n console.log('');\n console.log('Note: Tunnel list feature coming soon.');\n}\n","export async function stopCommand(tunnelId: string): Promise<void> {\n console.log(`Stopping tunnel: ${tunnelId}`);\n console.log('');\n console.log('Note: Remote stop feature coming soon.');\n}\n"],"mappings":"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","names":["React","useState","useEffect","Box","Text","Box","Text","jsx","jsxs","Box","Text","jsx","jsxs","jsx","jsxs","useState","useEffect","Box","Text","WebSocket","EventEmitter","EventEmitter","WebSocket","React"]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "untunneled.dev",
3
+ "version": "0.1.0",
4
+ "description": "Privacy-first localhost tunneling - P2P by default",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "untunneled": "./dist/index.js",
9
+ "untunneled.dev": "./dist/index.js"
10
+ },
11
+ "keywords": [
12
+ "tunnel",
13
+ "localhost",
14
+ "ngrok",
15
+ "p2p",
16
+ "privacy",
17
+ "webrtc",
18
+ "peer-to-peer",
19
+ "cli"
20
+ ],
21
+ "author": "",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "chalk": "^5.3.0",
25
+ "commander": "^14.0.2",
26
+ "ink": "^6.6.0",
27
+ "nanoid": "^5.0.7",
28
+ "posthog-node": "^5.21.0",
29
+ "qrcode-terminal": "^0.12.0",
30
+ "react": "^19.2.3",
31
+ "simple-peer": "^9.11.1",
32
+ "ws": "^8.18.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.0.9",
36
+ "@types/react": "^19.2.8",
37
+ "@types/simple-peer": "^9.11.8",
38
+ "@types/ws": "^8.5.10",
39
+ "tsup": "^8.1.0",
40
+ "typescript": "^5.5.0",
41
+ "vitest": "^4.0.17"
42
+ },
43
+ "engines": {
44
+ "node": ">=20.0.0"
45
+ },
46
+ "files": [
47
+ "dist"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "clean": "rm -rf dist"
56
+ }
57
+ }