@dpkrn/nodetunnel 1.1.0 → 1.1.1
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 +39 -43
- package/cmd/test-lib/main.js +5 -1
- package/internal/inspector/inspector.js +5 -9
- package/internal/inspector/logstore.js +11 -0
- package/internal/tunnel/tunnel.js +21 -12
- package/package.json +1 -1
- package/pkg/tunnel/tunnel.js +1 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ From **Node.js**, you get a **public URL** for webhooks, demos, sharing a dev se
|
|
|
16
16
|
- No port forwarding or firewall configuration needed
|
|
17
17
|
- Works behind NAT or private networks
|
|
18
18
|
- Simple integration with existing Node.js HTTP servers (Express, Fastify, plain `http`, etc.)
|
|
19
|
-
-
|
|
19
|
+
- Optional **traffic inspector**: local dashboard to capture tunneled requests, inspect them, and replay against your app
|
|
20
20
|
|
|
21
21
|
Incoming traffic reaches the public URL, is forwarded through the tunnel, and is proxied to your local HTTP server (e.g., `localhost:8080`).
|
|
22
22
|
|
|
@@ -40,7 +40,7 @@ This enables exposing local development servers without port forwarding, firewal
|
|
|
40
40
|
- **No separate tunnel process** — call one function from your app.
|
|
41
41
|
- **Works with your existing server** — Express, Fastify, or plain `http`.
|
|
42
42
|
- **Simple API** — you get a public `url` and a `stop()` when you are done.
|
|
43
|
-
- **Optional traffic inspector** — local
|
|
43
|
+
- **Optional traffic inspector** — pass `inspector: true` to start a local HTTP UI on loopback (captures, replay, headers/bodies). By default the inspector is **off**. Themes are chosen **inside the inspector UI** (not via `startTunnel` options).
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
@@ -54,8 +54,6 @@ This package is **ESM** (`import` / `export`).
|
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
57
|
## Quick Example
|
|
60
58
|
|
|
61
59
|
```js
|
|
@@ -69,8 +67,8 @@ app.get("/", (req, res) => res.send("OK"));
|
|
|
69
67
|
|
|
70
68
|
app.listen(PORT, async () => {
|
|
71
69
|
const { url, stop } = await startTunnel(String(PORT));
|
|
72
|
-
//url
|
|
73
|
-
//stop()
|
|
70
|
+
// `url` — public URL for your server
|
|
71
|
+
// `stop()` — closes the tunnel connection
|
|
74
72
|
});
|
|
75
73
|
```
|
|
76
74
|
|
|
@@ -107,29 +105,33 @@ Run with `node app.js`. Open the printed URL in a browser or share it for webhoo
|
|
|
107
105
|
|
|
108
106
|
---
|
|
109
107
|
|
|
110
|
-
## Traffic inspector (local dashboard)
|
|
108
|
+
## Traffic inspector (optional local dashboard)
|
|
109
|
+
|
|
110
|
+
**Default:** `inspector` is **`false`** if you omit options or do not set `inspector`. No inspector server, no capture buffer, no extra listen port.
|
|
111
111
|
|
|
112
|
-
When
|
|
112
|
+
When **`inspector: true`**:
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
- **Inspect** — request/response headers and bodies for each capture.
|
|
117
|
-
- **Modify** — modify request header/path and can replay.
|
|
118
|
-
- **Replay** — send a capture again to your local app, or edit method/path/headers/body and replay (aligned with the **gotunnel** inspector behavior).
|
|
114
|
+
1. Starts a small **HTTP server on your machine** (default listen **`http://127.0.0.1:4040`**) that serves the inspector UI (override with `inspectorAddr`).
|
|
115
|
+
2. **Captures** each tunneled request/response in memory (up to **`logs`** entries) so the UI can list them and push updates over WebSocket.
|
|
119
116
|
|
|
120
|
-
|
|
117
|
+
When **`inspector` stays false** (the default):
|
|
121
118
|
|
|
122
|
-
|
|
119
|
+
- No inspector process runs — nothing listens on the inspector port.
|
|
120
|
+
- No traffic is stored for inspection (`logs` and `inspectorAddr` are ignored).
|
|
121
|
+
- The startup banner omits the **Inspector →** line.
|
|
123
122
|
|
|
124
|
-
|
|
123
|
+
### What you get with the inspector enabled
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
| **`"light"`** | Light gray/white background, high-contrast text for bright environments. |
|
|
125
|
+
- **Live traffic** — tunneled requests appear in the UI (WebSocket updates).
|
|
126
|
+
- **History** — recent captures in memory (size limited by `logs`).
|
|
127
|
+
- **Inspect** — request/response headers and bodies (when captured).
|
|
128
|
+
- **Replay** — send a capture again to your local app, or edit method/path/headers/body and replay.
|
|
131
129
|
|
|
132
|
-
###
|
|
130
|
+
### Themes (inspector UI only)
|
|
131
|
+
|
|
132
|
+
Postman-style and Terminal-style palettes are available from the **Theme** dropdown in the inspector page; the choice is stored in the browser (localStorage). You do **not** configure themes on `startTunnel`.
|
|
133
|
+
|
|
134
|
+
### Example: inspector enabled with custom port and log limit
|
|
133
135
|
|
|
134
136
|
```js
|
|
135
137
|
import http from "node:http";
|
|
@@ -144,16 +146,11 @@ const server = http.createServer((req, res) => {
|
|
|
144
146
|
|
|
145
147
|
server.listen(PORT, async () => {
|
|
146
148
|
const { url, stop } = await startTunnel(String(PORT), {
|
|
147
|
-
// Inspector (defaults: enabled, :4040, dark theme, 100 logs)
|
|
148
149
|
inspector: true,
|
|
149
150
|
inspectorAddr: ":4040",
|
|
150
|
-
themes: "terminal", // try: "dark" | "terminal" | "light"
|
|
151
151
|
logs: 100,
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
// console.log("Public:", url);
|
|
155
|
-
// Open the Inspector URL from stderr in a browser (e.g. http://localhost:4040)
|
|
156
|
-
|
|
157
154
|
process.once("SIGINT", () => {
|
|
158
155
|
stop();
|
|
159
156
|
server.close(() => process.exit(0));
|
|
@@ -161,14 +158,14 @@ server.listen(PORT, async () => {
|
|
|
161
158
|
});
|
|
162
159
|
```
|
|
163
160
|
|
|
164
|
-
|
|
161
|
+
Open the **Inspector →** URL printed on startup (e.g. `http://127.0.0.1:4040`).
|
|
165
162
|
|
|
166
|
-
|
|
167
|
-
import { startTunnel } from "@dpkrn/nodetunnel";
|
|
163
|
+
### Example: tunnel only (default — same as omitting options)
|
|
168
164
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
`startTunnel(port)` / `startTunnel(port, {})` already keeps the inspector off. You only need `inspector: false` if you merge options from elsewhere and want to force it off:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
const { url, stop } = await startTunnel("8080"); // no inspector
|
|
172
169
|
```
|
|
173
170
|
|
|
174
171
|
---
|
|
@@ -207,27 +204,26 @@ app.listen(PORT, async () => {
|
|
|
207
204
|
| Argument | Description |
|
|
208
205
|
|----------|-------------|
|
|
209
206
|
| `port` | String, e.g. `"8080"` — must match the port your HTTP server uses. |
|
|
210
|
-
| `options` | Optional.
|
|
207
|
+
| `options` | Optional. See **Options** below. |
|
|
211
208
|
|
|
212
209
|
**Returns:** `{ url, stop }`
|
|
213
210
|
|
|
214
211
|
| Field | Description |
|
|
215
212
|
|-------|-------------|
|
|
216
213
|
| `url` | Public URL people can hit. |
|
|
217
|
-
| `stop` | Call to tear down the tunnel. |
|
|
214
|
+
| `stop` | Call to tear down the tunnel (and the inspector, if it was started). |
|
|
218
215
|
|
|
219
216
|
Errors **reject** the promise — use `try/catch`.
|
|
220
217
|
|
|
221
218
|
### Options (`startTunnel` second argument)
|
|
222
219
|
|
|
223
|
-
| Field | Type | Default |
|
|
224
|
-
|
|
225
|
-
| `host` | `string` | `'clickly.cv'` |
|
|
226
|
-
| `serverPort` | `number` | `9000` | TCP port of the tunnel server. |
|
|
227
|
-
| `inspector` | `boolean` | `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `inspectorAddr` | `string` | `':4040'` | Listen address for the inspector (e.g. `':4040'`, `'localhost:9090'`). Display URL follows the same rules as the public banner. |
|
|
220
|
+
| Field | Type | Default | Applies when |
|
|
221
|
+
|-------|------|---------|----------------|
|
|
222
|
+
| `host` | `string` | `'clickly.cv'` | Always — tunnel control server hostname. |
|
|
223
|
+
| `serverPort` | `number` | `9000` | Always — TCP port of the tunnel server. |
|
|
224
|
+
| `inspector` | `boolean` | `false` | Always — if `false` (default), no inspector UI, no capture store, and inspector-only options below are ignored. Set `true` to enable the local inspector. |
|
|
225
|
+
| `logs` | `number` | `100` | **`inspector: true` only** — max request/response captures kept in memory. |
|
|
226
|
+
| `inspectorAddr` | `string` | `':4040'` | **`inspector: true` only** — listen address for the inspector (e.g. `':4040'`, `'localhost:9090'`). |
|
|
231
227
|
|
|
232
228
|
---
|
|
233
229
|
|
package/cmd/test-lib/main.js
CHANGED
|
@@ -44,7 +44,11 @@ app.post("/test/:category/:itemId", (req, res) => {
|
|
|
44
44
|
app.listen(PORT, async () => {
|
|
45
45
|
console.log(`listening on http://localhost:${PORT}`);
|
|
46
46
|
try {
|
|
47
|
-
const { url, stop } = await startTunnel(String(PORT)
|
|
47
|
+
const { url, stop } = await startTunnel(String(PORT),{
|
|
48
|
+
inspector: true,
|
|
49
|
+
inspectorAddr: ':5040',
|
|
50
|
+
logs: 100,
|
|
51
|
+
});
|
|
48
52
|
process.once("SIGINT", () => {
|
|
49
53
|
stop();
|
|
50
54
|
process.exit(0);
|
|
@@ -43,13 +43,9 @@ export function inspectorHTTPBaseURL(opts) {
|
|
|
43
43
|
return `http://${addr}`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
.trim()
|
|
50
|
-
.toLowerCase();
|
|
51
|
-
if (t === 'terminal') return 'terminal';
|
|
52
|
-
return 'postman';
|
|
46
|
+
/** Default HTML theme before localStorage applies (UI also has a theme switcher). */
|
|
47
|
+
function defaultInspectorThemeSeed() {
|
|
48
|
+
return 'terminal';
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
/** @param {string} host */
|
|
@@ -409,7 +405,7 @@ function parseListenAddr(addr) {
|
|
|
409
405
|
}
|
|
410
406
|
|
|
411
407
|
/**
|
|
412
|
-
* @param {{ inspector?: boolean;
|
|
408
|
+
* @param {{ inspector?: boolean; inspectorAddr?: string }} opts
|
|
413
409
|
* @param {string} localPort digits — forwarded app port for default replay base in UI
|
|
414
410
|
* @returns {() => void}
|
|
415
411
|
*/
|
|
@@ -418,7 +414,7 @@ export function startInspector(opts, localPort) {
|
|
|
418
414
|
return () => {};
|
|
419
415
|
}
|
|
420
416
|
|
|
421
|
-
const themeSeed =
|
|
417
|
+
const themeSeed = defaultInspectorThemeSeed();
|
|
422
418
|
let addr = String(opts.inspectorAddr ?? '').trim();
|
|
423
419
|
if (!addr) addr = defaultInspectorAddr;
|
|
424
420
|
|
|
@@ -10,6 +10,9 @@ let requestLogs = [];
|
|
|
10
10
|
/** @type {((entry: Record<string, unknown>) => void) | null} */
|
|
11
11
|
let inspectorSubscriber = null;
|
|
12
12
|
|
|
13
|
+
/** When false (tunnel started with `inspector: false`), captures are not stored. */
|
|
14
|
+
let trafficCaptureEnabled = true;
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Wire live inspector WebSocket broadcast (optional).
|
|
15
18
|
* @param {((entry: Record<string, unknown>) => void) | null} fn
|
|
@@ -18,6 +21,13 @@ export function setInspectorSubscriber(fn) {
|
|
|
18
21
|
inspectorSubscriber = fn;
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @param {boolean} enabled
|
|
26
|
+
*/
|
|
27
|
+
export function setTrafficCaptureEnabled(enabled) {
|
|
28
|
+
trafficCaptureEnabled = !!enabled;
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
/**
|
|
22
32
|
* @param {number} n
|
|
23
33
|
*/
|
|
@@ -33,6 +43,7 @@ export function setMaxRequestLogs(n) {
|
|
|
33
43
|
* @param {Record<string, unknown>} entry
|
|
34
44
|
*/
|
|
35
45
|
export function addLog(entry) {
|
|
46
|
+
if (!trafficCaptureEnabled) return;
|
|
36
47
|
requestLogs.push(entry);
|
|
37
48
|
if (requestLogs.length > maxRequestLogs) {
|
|
38
49
|
requestLogs = requestLogs.slice(requestLogs.length - maxRequestLogs);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
import { Session } from 'yamux-js/lib/session.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
addLog,
|
|
6
|
+
newLogId,
|
|
7
|
+
setMaxRequestLogs,
|
|
8
|
+
setTrafficCaptureEnabled,
|
|
9
|
+
} from '../inspector/logstore.js';
|
|
5
10
|
import { startInspector } from '../inspector/inspector.js';
|
|
6
11
|
// import { version } from '../../../package.json' with { type: 'json' };
|
|
7
12
|
|
|
@@ -167,18 +172,17 @@ async function handleStream(stream, port) {
|
|
|
167
172
|
* @typedef {Object} TunnelOptions
|
|
168
173
|
* @property {string} [host] tunnel server host (default clickly.cv)
|
|
169
174
|
* @property {number} [serverPort] tunnel server TCP port (default 9000)
|
|
170
|
-
* @property {boolean} [inspector] traffic inspector UI (default
|
|
171
|
-
* @property {
|
|
172
|
-
* @property {
|
|
173
|
-
* @property {string} [inspectorAddr] inspector listen address (default ":4040")
|
|
175
|
+
* @property {boolean} [inspector] traffic inspector UI (default false). When false, no inspector server, no in-memory traffic capture, and `logs` / `inspectorAddr` are ignored.
|
|
176
|
+
* @property {number} [logs] max request/response captures in memory for the inspector (default 100). Only used when `inspector` is true.
|
|
177
|
+
* @property {string} [inspectorAddr] inspector listen address (default ":4040"). Only used when `inspector` is true.
|
|
174
178
|
*/
|
|
175
179
|
|
|
176
180
|
function defaultTunnelOptions() {
|
|
177
181
|
return {
|
|
178
182
|
host: 'clickly.cv',
|
|
179
183
|
serverPort: 9000,
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
themes: 'terminal',
|
|
185
|
+
inspector: false,
|
|
182
186
|
logs: 100,
|
|
183
187
|
inspectorAddr: '',
|
|
184
188
|
};
|
|
@@ -193,13 +197,15 @@ function applyTunnelOptions(options) {
|
|
|
193
197
|
if (!options || typeof options !== 'object') {
|
|
194
198
|
return /** @type {TunnelOptions & typeof d} */ ({ ...d });
|
|
195
199
|
}
|
|
200
|
+
const inspector =
|
|
201
|
+
options.inspector !== undefined ? !!options.inspector : d.inspector;
|
|
196
202
|
return {
|
|
197
203
|
host: options.host ?? d.host,
|
|
198
204
|
serverPort: options.serverPort ?? d.serverPort,
|
|
199
|
-
inspector
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
inspectorAddr: options.inspectorAddr ?? d.inspectorAddr,
|
|
205
|
+
inspector,
|
|
206
|
+
logs:
|
|
207
|
+
inspector && options.logs > 0 ? options.logs : d.logs,
|
|
208
|
+
inspectorAddr: inspector ? (options.inspectorAddr ?? d.inspectorAddr) : d.inspectorAddr,
|
|
203
209
|
};
|
|
204
210
|
}
|
|
205
211
|
|
|
@@ -306,7 +312,10 @@ class Tunnel {
|
|
|
306
312
|
*/
|
|
307
313
|
async function newTunnel(localPort, options) {
|
|
308
314
|
const opts = applyTunnelOptions(options);
|
|
309
|
-
|
|
315
|
+
setTrafficCaptureEnabled(opts.inspector);
|
|
316
|
+
if (opts.inspector) {
|
|
317
|
+
setMaxRequestLogs(opts.logs);
|
|
318
|
+
}
|
|
310
319
|
const t = new Tunnel(localPort, opts);
|
|
311
320
|
await t.connect();
|
|
312
321
|
t._stopInspector = startInspector(opts, localPort);
|
package/package.json
CHANGED
package/pkg/tunnel/tunnel.js
CHANGED
|
@@ -51,10 +51,9 @@ function printSuccess(publicURL, localURL, inspectorURL) {
|
|
|
51
51
|
* host?: string,
|
|
52
52
|
* serverPort?: number,
|
|
53
53
|
* inspector?: boolean,
|
|
54
|
-
* themes?: 'dark' | 'terminal' | 'light' | string,
|
|
55
54
|
* logs?: number,
|
|
56
55
|
* inspectorAddr?: string,
|
|
57
|
-
* }} [options]
|
|
56
|
+
* }} [options] Omit or leave defaults for tunnel-only (`inspector` defaults to false).
|
|
58
57
|
* @returns {Promise<{ url: string, stop: () => void }>}
|
|
59
58
|
*/
|
|
60
59
|
async function startTunnel(port, options) {
|