@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 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
- - Traffic inspector: capture traffic, replay, and modify requests as many times as you need
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 dashboard on loopback to browse captures, replay requests, modify and pick a theme (see below).
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 is your public url using you can access publicly your server
73
- //stop() is method that will close your connection on error
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 enabled (default), nodetunnel starts a small **HTTP server on your machine** (default `http://localhost:4040`) with:
112
+ When **`inspector: true`**:
113
113
 
114
- - **Live traffic** requests proxied through the tunnel appear in the UI (WebSocket updates).
115
- - **History** recent captures kept in memory (configurable count); reload the page to fetch `/logs`.
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
- The startup banner prints **Inspector →** with that URL. Set `inspector: false` if you do not want the UI or an extra listen port.
117
+ When **`inspector` stays false** (the default):
121
118
 
122
- ### Themes
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
- The UI supports three built-in palettes via `themes` in `startTunnel` options:
123
+ ### What you get with the inspector enabled
125
124
 
126
- | Value | Appearance |
127
- |--------|----------------|
128
- | **`"dark"`** (default) | Dark panels, blue accents — similar to GitHub-dark style. |
129
- | **`"terminal"`** | Green-on-black “CRT” / terminal aesthetic, monospace UI font. |
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
- ### Example: themes and inspector options
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
- ### Example: tunnel only (no inspector)
161
+ Open the **Inspector →** URL printed on startup (e.g. `http://127.0.0.1:4040`).
165
162
 
166
- ```js
167
- import { startTunnel } from "@dpkrn/nodetunnel";
163
+ ### Example: tunnel only (default — same as omitting options)
168
164
 
169
- const { url, stop } = await startTunnel("8080", {
170
- inspector: false,
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. Object — see **Options** below (tunnel server address, inspector, themes, etc.). |
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 | Description |
224
- |-------|------|---------|-------------|
225
- | `host` | `string` | `'clickly.cv'` | Hostname of the tunnel **control** server. |
226
- | `serverPort` | `number` | `9000` | TCP port of the tunnel server. |
227
- | `inspector` | `boolean` | `true` | If `true`, start the local traffic inspector UI (see [Traffic inspector](#traffic-inspector-local-dashboard)). If `false`, no extra HTTP server and no Inspector line in the banner. |
228
- | `themes` | `string` | `'dark'` | Inspector palette: `'dark'`, `'terminal'`, or `'light'`. |
229
- | `logs` | `number` | `100` | Maximum number of request/response captures kept in memory for the inspector. |
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
 
@@ -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
- /** @param {string | undefined} themes */
47
- function themeSeedFromOpts(themes) {
48
- const t = String(themes ?? '')
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; themes?: string; inspectorAddr?: string }} opts
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 = themeSeedFromOpts(opts.themes);
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 { addLog, newLogId, setMaxRequestLogs } from '../inspector/logstore.js';
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 true)
171
- * @property {string} [themes] inspector palette: "dark" | "terminal" | "light"
172
- * @property {number} [logs] max request logs in memory (default 100)
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
- inspector: true,
181
- themes: 'dark',
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: options.inspector !== undefined ? !!options.inspector : d.inspector,
200
- themes: options.themes ?? d.themes,
201
- logs: options.logs > 0 ? options.logs : d.logs,
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
- setMaxRequestLogs(opts.logs);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dpkrn/nodetunnel",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Expose a local HTTP server through a devtunnel/gotunnel-compatible server (yamux + JSON). Node.js 18+.",
5
5
  "keywords": [
6
6
  "tunnel",
@@ -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) {