@yousolution/node-red-contrib-you-tunnel-websocket 1.1.2 → 1.2.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/AGENTS.md ADDED
@@ -0,0 +1,45 @@
1
+ # AI Development Rules
2
+
3
+ Before modifying production code:
4
+
5
+ 1. Read `STABILITY_CONTRACT.md`.
6
+ 2. Identify the RWT-\* invariants affected by the change.
7
+ 3. Inspect the relevant implementation and existing tests.
8
+ 4. Do not weaken or remove tests to make them pass.
9
+ 5. Every bug fix must include a regression test when technically possible.
10
+ 6. Do not increase timeouts to hide races.
11
+ 7. Do not add retries without identifying the root cause.
12
+ 8. Run all pertinent tests after every modification.
13
+ 9. Do not use `--forceExit` to hide resource leaks.
14
+ 10. Keep changes minimal and scoped to the current task.
15
+ 11. Do not refactor unrelated code.
16
+
17
+ ## Stability Contract
18
+
19
+ `STABILITY_CONTRACT.md` is normative.
20
+
21
+ Do not weaken, remove, or modify an RWT-\* invariant merely to make an implementation or test pass.
22
+
23
+ If the intended system behavior changes:
24
+
25
+ 1. Update `STABILITY_CONTRACT.md`.
26
+ 2. Explain why the invariant changed.
27
+ 3. Add or update the relevant regression test.
28
+ 4. Verify that dependent invariants remain satisfied.
29
+
30
+ Known Issues, Stability Risks, and Open Questions in the Stability Contract are not invariants and must not be treated as requirements without additional evidence.
31
+
32
+ ## Workflow
33
+
34
+ For each task:
35
+
36
+ 1. Read `STABILITY_CONTRACT.md`.
37
+ 2. Inspect the relevant implementation.
38
+ 3. Inspect existing tests.
39
+ 4. Identify affected invariants, known issues, and stability risks.
40
+ 5. Write or reproduce the regression test.
41
+ 6. Make the smallest production change necessary.
42
+ 7. Run the focused test.
43
+ 8. Run the relevant existing suites.
44
+ 9. Verify affected RWT-\* invariants.
45
+ 10. Report changed files, tests executed, invariant impact, and remaining risks.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.2.0] - 2026-09-10
11
+
12
+ ### Added
13
+ - New `wstunnel command` node for sending commands to connected tunnel clients (fire-and-forget)
14
+ - `wstunnel server` node output: emits `connect` and `disconnect` events when tunnels register or close
15
+ - `wstunnel server` node input: accepts any message to trigger a server metrics snapshot via `getMetrics().snapshot()`
16
+ - Metrics snapshot includes: `active_tunnels`, `active_streams`, `bytes_in_total`, `bytes_out_total`, `event_loop_lag_ms`, per-tunnel `tunnels_detail`
17
+ - `STABILITY_CONTRACT.md` — 4 RWT invariants (RWT-001 through RWT-004) with regression tests
18
+ - `AGENTS.md` — AI development rules and workflow
19
+ - Regression tests for all KI fixes, output events, metrics input, concurrent deployment, and command node
20
+
21
+ ### Fixed
22
+ - KI-001: Inverted host/path default logic — configured values were discarded
23
+ - KI-002: tunnelIdHeaderName HTML default mismatch — editor default did not match runtime
24
+ - KI-003: Leftover debug log statement at info level — changed to debug level
25
+ - KI-004: console.error used in production code — replaced with logger.error
26
+ - KI-005: License inconsistency — package.json declared MIT while source files declared Apache-2.0
27
+ - `data/flows.json`: Fixed `host` and `tunnelIdHeaderName` for Docker compatibility
28
+
29
+ ### Changed
30
+ - `wstunnel server` node now accepts input (1 input, was 0)
31
+ - Replaced `setTimeout` with deterministic `setImmediate`/`Promise.resolve` in all test files
32
+ - Updated `@remotelinker/reverse-ws-tunnel` dependency
33
+
34
+ ---
35
+
10
36
  ## [1.1.2] - 2026-05-04
11
37
 
12
38
  ### Fixed
@@ -75,6 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
75
101
 
76
102
  ---
77
103
 
104
+ [1.2.0]: https://github.com/yousolution-cloud/node-red-contrib-you-tunnel-websocket/compare/v1.1.2...v1.2.0
78
105
  [1.1.2]: https://github.com/yousolution-cloud/node-red-contrib-you-tunnel-websocket/compare/v1.1.1...v1.1.2
79
106
  [1.1.1]: https://github.com/yousolution-cloud/node-red-contrib-you-tunnel-websocket/compare/v1.1.0...v1.1.1
80
107
  [1.1.0]: https://github.com/yousolution-cloud/node-red-contrib-you-tunnel-websocket/compare/v1.0.1...v1.1.0
package/README.md CHANGED
@@ -39,11 +39,22 @@ Creates a WebSocket server that can tunnel HTTP traffic over WebSocket connectio
39
39
 
40
40
  #### Inputs
41
41
 
42
- *none*
42
+ | Property | Type | Description |
43
+ |---|---|---|
44
+ | `*` | `any` | Send any message to request a metrics snapshot on the output |
43
45
 
44
46
  #### Outputs
45
47
 
46
- *none*
48
+ Emits messages when tunnels connect, disconnect, or when metrics are requested:
49
+
50
+ | Property | Type | Description |
51
+ |---|---|---|
52
+ | `event` | `string` | `"connect"` when a tunnel is registered, `"disconnect"` when it closes, `"metrics"` when a snapshot is returned |
53
+ | `tunnelId` | `string` | The unique tunnel identifier (UUID) — present on connect/disconnect events |
54
+ | `remoteAddress` | `string` | The client IP address — present on connect/disconnect events |
55
+ | `connectedAt` | `number` | `Date.now()` when the tunnel was registered — present on connect events |
56
+ | `serverPort` | `number` | The port the WebSocket server is listening on |
57
+ | `payload` | `object` | On metrics events: the full snapshot object with aggregate and per-tunnel metrics |
47
58
 
48
59
  #### Details
49
60
 
@@ -54,6 +65,37 @@ Status indicators:
54
65
  - 🟢 **listening on port [port]**: Server is active and listening
55
66
  - 🟢 **clients [count]**: Number of connected WebSocket clients
56
67
 
68
+ ### wstunnel command
69
+
70
+ Sends commands to connected tunnel clients via WebSocket.
71
+
72
+ #### Properties
73
+
74
+ - **Name**: Optional name for the node
75
+ - **wstunnel**: Reference to a wstunnel configuration node *(required)*
76
+
77
+ #### Inputs
78
+
79
+ | Property | Type | Description |
80
+ |---|---|---|
81
+ | `payload` | `object` | Command payload (may contain `tunnelId`, `command`, `args`) |
82
+ | `tunnelId` | `string` | Target tunnel UUID (alternative to `payload.tunnelId`) |
83
+ | `command` | `string` | Command name to send, e.g. `"kill"` (alternative to `payload.command`) |
84
+ | `args` | `object` | Optional arguments for the command (alternative to `payload.args`) |
85
+
86
+ #### Outputs
87
+
88
+ Two outputs:
89
+
90
+ - **Output 0 (success)**: Command delivered. `msg.payload` is `true`.
91
+ - **Output 1 (failure)**: Tunnel not found or not connected. `msg.payload` is `false`, `msg.error` contains the reason.
92
+
93
+ #### Details
94
+
95
+ The node sends a command to a connected tunnel client. The target tunnel is identified by its `tunnelId`. If the tunnel exists and its WebSocket is open, the command is delivered and output 0 fires. If the tunnel does not exist or is disconnected, output 1 fires with an error message.
96
+
97
+ Missing `tunnelId` or `command` sends the message to output 1 with an error.
98
+
57
99
  ## Usage
58
100
 
59
101
  ### Basic Setup
@@ -67,6 +109,44 @@ Status indicators:
67
109
 
68
110
  3. The server will start listening for WebSocket connections
69
111
 
112
+ ### Monitoring Connections
113
+
114
+ Connect the **wstunnel server** output to a `switch` node to route connect and disconnect events:
115
+
116
+ ```
117
+ [wstunnel server] → [switch: event == "connect"] → [notification flow]
118
+ → [switch: event == "disconnect"] → [cleanup flow]
119
+ ```
120
+
121
+ ### Monitoring Metrics
122
+
123
+ Wire an `inject` node to the **wstunnel server** input to periodically request metrics:
124
+
125
+ ```
126
+ [inject: every 5s] → [wstunnel server] → [debug: metrics]
127
+ ```
128
+
129
+ The output `msg.payload` contains:
130
+
131
+ | Field | Description |
132
+ |---|---|
133
+ | `active_tunnels` | Number of currently active tunnels |
134
+ | `active_streams` | Number of active TCP streams |
135
+ | `bytes_in_total / bytes_out_total` | Aggregate byte counters |
136
+ | `event_loop_lag_ms` | `{ p50, p99 }` event loop lag |
137
+ | `tunnels_detail` | Per-tunnel: `remoteAddress`, `streamCount`, `bytesIn`, `bytesOut`, `agentVersion` |
138
+
139
+ ### Sending Commands to Clients
140
+
141
+ Wire a trigger to a **wstunnel command** node to send commands to connected clients:
142
+
143
+ ```
144
+ [inject: {tunnelId: "uuid", command: "kill"}]
145
+ → [wstunnel command]
146
+ → output 0 → [debug: success]
147
+ → output 1 → [debug: failure]
148
+ ```
149
+
70
150
  ### Connection URLs
71
151
 
72
152
  WebSocket servers are accessible at:
@@ -92,5 +172,4 @@ Apache-2.0
92
172
 
93
173
  Andrea Trentin (<andrea.trentin@yousolution.cloud>)
94
174
 
95
- Repository: [yousolution-cloud/node-red-contrib-you-tunnel-websocket](https://github.com/yousolution-cloud/node-red-contrib-you-tunnel-websocket)</content>
96
- <parameter name="filePath">README.md
175
+ Repository: [yousolution-cloud/node-red-contrib-you-tunnel-websocket](https://github.com/yousolution-cloud/node-red-contrib-you-tunnel-websocket)
@@ -0,0 +1,299 @@
1
+ # Stability Contract
2
+
3
+ This document is normative. It defines the project's **RWT-\*** invariants — properties that must be preserved across all production changes.
4
+
5
+ ## What Is an Invariant?
6
+
7
+ An RWT-\* invariant is a property of the system that must remain true across production changes.
8
+
9
+ An invariant describes required behavior or compatibility, not a particular implementation.
10
+
11
+ Implementation details may change as long as the invariant remains satisfied.
12
+
13
+ Known bugs, temporary workarounds, coding conventions, and developer workflow rules are not invariants unless explicitly justified as stability requirements.
14
+
15
+ ## How to Use This Contract
16
+
17
+ Before modifying production code:
18
+
19
+ 1. Identify which RWT-\* invariants may be affected by the change.
20
+ 2. Verify the tests that protect those invariants (if they exist).
21
+ 3. Evaluate whether the change preserves all affected invariants.
22
+ 4. Add or update regression tests when necessary.
23
+ 5. If an invariant must intentionally change, update this file first.
24
+
25
+ This contract does not replace AGENTS.md. It supplements it with stability-specific constraints.
26
+
27
+ ## Contract Scope
28
+
29
+ This document defines stability properties only. It must not be used to justify unrelated refactoring, cleanup, or behavior changes.
30
+
31
+ The contract describes what must remain true, not how the implementation must achieve it.
32
+
33
+ ## Evidence and Authority
34
+
35
+ The Stability Contract is normative, but each invariant should be supported by repository evidence.
36
+
37
+ Evidence may include:
38
+
39
+ - Production code
40
+ - Automated tests
41
+ - Integration tests
42
+ - Project documentation
43
+ - CHANGELOG entries
44
+ - Commit history
45
+ - External framework contracts relied upon by the project
46
+
47
+ When evidence is insufficient to establish a required behavior, document the behavior as a Known Issue or Open Question rather than promoting it to an invariant.
48
+
49
+ ---
50
+
51
+ ## Invariants
52
+
53
+ ### RWT-001 — Exclusive Server Ownership of a Port
54
+
55
+ **Status:** Active
56
+
57
+ **Invariant**
58
+
59
+ A replacement managed WebSocket server must not be started on a port until the previously managed server on that port has completed its shutdown. At no point during a replacement operation may the implementation intentionally start the replacement while the previous server is still active.
60
+
61
+ **Why it matters**
62
+
63
+ If two servers attempt to bind the same port simultaneously, the second will fail with a port-in-use error, or both may enter an undefined state. This prevents orphaned server processes from accumulating across Node-RED redeployments.
64
+
65
+ **Scope**
66
+
67
+ - `nodes/wsTunnel.js` — `WSTunnelServer` constructor
68
+
69
+ **Protected by**
70
+
71
+ Automated tests:
72
+
73
+ - `test/rwt-001-port-exclusivity.test.js` — 2 tests (mock-based ordering verification)
74
+ - `test/rwt-001-port-release.test.js` — 2 tests (deterministic port-binding verification)
75
+
76
+ Historical evidence:
77
+
78
+ - CHANGELOG v1.0.1: "Remove old connections when redeploying flows"
79
+ - CHANGELOG v1.1.2: "Add proper cleanup of existing server on port before starting"
80
+ - Commit `0f2b273`: "fix: remove old connection when deploy"
81
+
82
+ **Potential violations**
83
+
84
+ - Starting a new server before the previous stop completes.
85
+ - Removing the cleanup-before-start sequence.
86
+ - Parallel deployments of multiple `wstunnel server` nodes on the same port without serializing the cleanup.
87
+
88
+ **Verification**
89
+
90
+ Deploy two `wstunnel server` nodes on the same port sequentially. Verify via Node-RED logs that the cleanup message appears before the start message for each deployment.
91
+
92
+ ---
93
+
94
+ ### RWT-002 — Node Shutdown Must Signal Completion
95
+
96
+ **Status:** Active
97
+
98
+ **Invariant**
99
+
100
+ When a `WSTunnelServer` node receives a Node-RED close event, its shutdown handler must always signal completion by invoking `done()`, including failure paths.
101
+
102
+ **Why it matters**
103
+
104
+ Node-RED uses the `done()` callback to determine that a node has finished its shutdown sequence. If `done()` is not called, Node-RED will hang indefinitely, preventing redeployment or shutdown of the flow.
105
+
106
+ **Scope**
107
+
108
+ - `nodes/wsTunnel.js` — `WSTunnelServer.on('close')` handler
109
+
110
+ **Protected by**
111
+
112
+ Automated tests:
113
+
114
+ - `test/rwt-002-shutdown-completion.test.js` — 4 tests
115
+
116
+ Historical evidence:
117
+
118
+ - Node-RED framework contract: `done()` must be called in `on('close')`.
119
+ - The `done()` call exists in both success and error branches of the current implementation.
120
+
121
+ **Potential violations**
122
+
123
+ - Adding a new code path in `on('close')` that does not call `done()`.
124
+ - Throwing an exception before `done()` is reached without a catch.
125
+ - Removing the `done()` call in the error branch.
126
+
127
+ **Verification**
128
+
129
+ Deploy a `wstunnel server` node, then remove or redeploy it. Verify that Node-RED does not hang on the deployment.
130
+
131
+ ---
132
+
133
+ ### RWT-003 — Node Runtime/Editor Compatibility
134
+
135
+ **Status:** Active
136
+
137
+ **Invariant**
138
+
139
+ Every Node-RED node type exposed by the package must have a corresponding editor definition, and the runtime and editor definitions must refer to the same node type. A node type must remain loadable and configurable through the Node-RED editor after production changes.
140
+
141
+ **Why it matters**
142
+
143
+ Node-RED uses the type name as the binding key between the editor UI (HTML) and the runtime (JS). A mismatch causes the node to be invisible in the editor, to fail to load, or to exhibit undefined behavior.
144
+
145
+ **Scope**
146
+
147
+ - `nodes/wsTunnel.js` — `RED.nodes.registerType()` calls
148
+ - `nodes/wsTunnel.html` — `data-template-name` and `data-help-name` attributes
149
+
150
+ **Protected by**
151
+
152
+ Automated tests:
153
+
154
+ - `test/rwt-003-runtime-editor-compatibility.test.js` — 5 tests
155
+
156
+ Historical evidence:
157
+
158
+ - Node-RED framework contract: type names must be consistent across JS and HTML.
159
+ - The current code has exact matches for the three active node types: `wstunnel`, `wstunnel server`, and `wstunnel command`.
160
+
161
+ **Potential violations**
162
+
163
+ - Renaming a type in JS without updating the HTML template and help attributes.
164
+ - Adding a new `RED.nodes.registerType()` call without corresponding HTML.
165
+ - Changing the type name of an existing node.
166
+
167
+ **Verification**
168
+
169
+ After modifying type registrations, load the Node-RED editor and verify that both node types appear in the palette. Verify that each can be dragged onto the canvas and configured.
170
+
171
+ ---
172
+
173
+ ### RWT-004 — Persisted Flow Configuration Compatibility
174
+
175
+ **Status:** Active
176
+
177
+ **Invariant**
178
+
179
+ Existing Node-RED flows using the currently supported `wstunnel` configuration properties must remain loadable after a package upgrade, and their persisted configuration values must retain their existing behavior. Removing or renaming a persisted configuration property requires an explicit migration strategy.
180
+
181
+ **Why it matters**
182
+
183
+ Node-RED stores node configuration in flow JSON files. If configuration properties are removed or renamed, existing flows will lose their configured values, causing silent failures or fallback to unexpected defaults. This breaks deployments that upgrade the module.
184
+
185
+ **Scope**
186
+
187
+ - `nodes/wsTunnel.js` — `WSTunnelNode` constructor (options object)
188
+ - `nodes/wsTunnel.html` — `wstunnel` registration defaults
189
+
190
+ **Protected by**
191
+
192
+ Automated tests:
193
+
194
+ - `test/rwt-004-persisted-config-compatibility.test.js` — 5 tests
195
+
196
+ Historical evidence:
197
+
198
+ - Node-RED framework contract: config node properties are persisted in flow JSON.
199
+ - `data/flows.json` stores `name`, `host`, `port`, `path`, `tunnelIdHeaderName`, and `logLevel` as persisted properties.
200
+
201
+ **Potential violations**
202
+
203
+ - Removing a property from the `options` object without deprecation.
204
+ - Renaming a property without providing a migration path.
205
+ - Changing the type or semantics of an existing property.
206
+
207
+ **Verification**
208
+
209
+ Create a flow JSON file with all current configuration properties set, then deploy the updated module. Verify that all values are preserved and functional in the editor.
210
+
211
+ ---
212
+
213
+ ## Known Stability Risks
214
+
215
+ These are potentially dangerous implementation characteristics that should be considered before modifying affected areas. Known Stability Risks are advisory. They must not be treated as requirements and must not be converted into invariants without additional evidence.
216
+
217
+ ### Risk: Global Mutable State in `instances` Object
218
+
219
+ - **Area:** `nodes/wsTunnel.js` line 19
220
+ - **Nature:** The `instances` object is module-level mutable state shared across all node instances. If two `wstunnel server` nodes deploy concurrently on the same port, the stop/start sequence may interleave, causing one node to overwrite the other's state.
221
+ - **Existing test:** None.
222
+ - **What to verify:** Before modifying server lifecycle logic, consider whether concurrency needs to be addressed with per-port locking or serialization.
223
+
224
+ ### Risk: Global Logger Context Override
225
+
226
+ - **Area:** `nodes/wsTunnel.js` lines 47–48
227
+ - **Nature:** `setLogContext` and `setLogLevel` affect the global logger. If multiple `wstunnel server` nodes are deployed, the last deployed node's context and level win for all nodes.
228
+ - **Existing test:** None.
229
+ - **What to verify:** Before modifying logger configuration, consider whether per-node logger instances are needed.
230
+
231
+ ---
232
+
233
+ ## Known Issues
234
+
235
+ Known defects or suspicious behavior discovered during contract analysis. Known Issues are non-normative and are documented for visibility only.
236
+
237
+ ### KI-001 — Inverted host/path Default Logic *(Resolved)*
238
+
239
+ - **Area:** `nodes/wsTunnel.js` lines 52–53
240
+ - **Nature:** The ternary expressions `node.tunnelConfig.options.host ? '' : undefined` were inverted. When `host` had a value (truthy), it was set to `''` (empty string). When falsy, it was set to `undefined`. The same applied to `path`. This meant configured host/path values were discarded.
241
+ - **Resolution:** Replaced with `host || undefined` and `path || undefined`. Configured values now pass through to `startWebSocketServer` correctly.
242
+ - **Regression test:** `test/ki-001-host-path-passthrough.test.js` (4 tests)
243
+
244
+ ### KI-002 — tunnelIdHeaderName Default Mismatch *(Resolved)*
245
+
246
+ - **Area:** `nodes/wsTunnel.js` line 32 vs `nodes/wsTunnel.html` line 77
247
+ - **Nature:** The constructor defaults `tunnelIdHeaderName` to `'x-tunnel-id'`, but the HTML registration defaulted it to `''`. When a user did not configure this field, the runtime value differed from the editor default.
248
+ - **Resolution:** Updated HTML default from `''` to `'x-tunnel-id'` to match constructor, documentation, and library examples.
249
+ - **Regression test:** `test/ki-002-tunnel-id-header-default.test.js` (3 tests)
250
+
251
+ ### KI-003 — Leftover Debug Log Statement *(Resolved)*
252
+
253
+ - **Area:** `nodes/wsTunnel.js` line 117
254
+ - **Nature:** `logger.info('[DEBUG] Close called for node ...')` was a debug statement left in production code using info level. It produced unnecessary log noise in every deployment.
255
+ - **Resolution:** Changed to `logger.debug(...)` and removed `[DEBUG]` prefix. Message now only appears when debug logging is enabled.
256
+ - **Regression test:** `test/ki-003-debug-log-level.test.js` (3 tests)
257
+
258
+ ### KI-004 — console.error in Production Code *(Resolved)*
259
+
260
+ - **Area:** `nodes/wsTunnel.js` line 127
261
+ - **Nature:** `console.error` was used directly instead of the project's `logger.error`. This bypassed log level filtering and produced inconsistent output format.
262
+ - **Resolution:** Replaced with `logger.error` to maintain logging consistency with the start-failure path.
263
+ - **Regression test:** `test/ki-004-console-error-usage.test.js` (1 test)
264
+
265
+ ### KI-005 — License Inconsistency *(Resolved)*
266
+
267
+ - **Area:** `README.md` (Apache-2.0), `package.json` (MIT), `nodes/wsTunnel.js` header (Apache-2.0), `nodes/wsTunnel.html` header (Apache-2.0).
268
+ - **Nature:** The license declarations were inconsistent across files. `package.json` declared MIT while all other files declared Apache-2.0.
269
+ - **Resolution:** Updated `package.json` from `"license": "MIT"` to `"license": "Apache-2.0"` to match the source files and README.
270
+
271
+ ---
272
+
273
+ ## Open Questions
274
+
275
+ These are behaviors important to the project for which the repository does not provide sufficient evidence to define a formal invariant. Open Questions are unknown and must not be assumed to be requirements.
276
+
277
+ *(No open questions remain.)*
278
+
279
+ ---
280
+
281
+ ## Contract Change Policy
282
+
283
+ A modification to an existing RWT-\* invariant requires:
284
+
285
+ 1. An explicit update to this file (STABILITY_CONTRACT.md).
286
+ 2. A written explanation of the changed semantics and the reason for the change.
287
+ 3. An update or addition of regression tests for the affected invariant.
288
+ 4. Verification that no dependent invariants are violated.
289
+
290
+ The Stability Contract must never be weakened, removed, or modified solely to make an implementation or test pass. A contract change is appropriate only when the intended system behavior has actually changed.
291
+
292
+ New invariants may only be added when:
293
+
294
+ - There is concrete evidence in the repository (code, tests, documentation, CHANGELOG, or commit history) that the property is intentionally protected.
295
+ - The invariant is formulated as a verifiable property, not a vague guideline.
296
+ - The invariant is not a restatement of a rule already in AGENTS.md.
297
+ - The invariant represents a meaningful stability or compatibility requirement.
298
+
299
+ Implementation details may change without modifying an invariant when the invariant itself remains satisfied.
@@ -74,7 +74,7 @@
74
74
  host: { value: '' },
75
75
  port: { value: '' },
76
76
  path: { value: '' },
77
- tunnelIdHeaderName: { value: '' },
77
+ tunnelIdHeaderName: { value: 'x-tunnel-id' },
78
78
  logLevel: { value: 'info' },
79
79
  },
80
80
  label: function () {
@@ -261,12 +261,57 @@
261
261
  The log level configured in the <code>wstunnel</code> config node is applied when the server starts, controlling the verbosity of logging output from the
262
262
  tunnel library.
263
263
  </p>
264
+ <p>
265
+ Send any message to the input to request a server metrics snapshot on the output. The snapshot includes aggregate counters and per-tunnel detail from the
266
+ tunnel library.
267
+ </p>
268
+
269
+ <h3>Output</h3>
270
+ <p>The single output emits messages when tunnels connect, disconnect, or when metrics are requested.</p>
271
+ <dl class="message-properties">
272
+ <dt>event <span class="property-type">string</span></dt>
273
+ <dd><code>"connect"</code> when a tunnel is registered, <code>"disconnect"</code> when it closes, <code>"metrics"</code> when a metrics snapshot is returned.</dd>
274
+
275
+ <dt>tunnelId <span class="property-type">string</span></dt>
276
+ <dd>The unique tunnel identifier (UUID). Present on connect and disconnect events.</dd>
277
+
278
+ <dt>remoteAddress <span class="property-type">string</span></dt>
279
+ <dd>The client IP address from the WebSocket upgrade request.</dd>
280
+
281
+ <dt>connectedAt <span class="property-type">number</span></dt>
282
+ <dd><code>Date.now()</code> when the tunnel was registered. Present on connect events.</dd>
283
+
284
+ <dt>serverPort <span class="property-type">number</span></dt>
285
+ <dd>The port the WebSocket server is listening on.</dd>
286
+
287
+ <dt>payload <span class="property-type">object</span></dt>
288
+ <dd>On metrics events, contains the full snapshot object with aggregate and per-tunnel metrics.</dd>
289
+ </dl>
290
+
291
+ <h3>Example</h3>
292
+ <p>Connect the output to a <code>switch</code> node to route events separately:</p>
293
+ <pre>[wstunnel server] → [switch: event == "connect"] → [notification flow]
294
+ → [switch: event == "disconnect"] → [cleanup flow]
295
+ → [switch: event == "metrics"] → [dashboard flow]</pre>
296
+
297
+ <h3>Metrics</h3>
298
+ <p>Send any message to the input to request a metrics snapshot. The output emits a message with <code>event: "metrics"</code> and <code>payload</code> containing:</p>
299
+ <ul>
300
+ <li><code>active_tunnels</code> — number of currently active tunnels</li>
301
+ <li><code>active_streams</code> — number of active TCP streams</li>
302
+ <li><code>bytes_in_total / bytes_out_total</code> — aggregate byte counters</li>
303
+ <li><code>event_loop_lag_ms</code> — <code>{ p50, p99 }</code> event loop lag</li>
304
+ <li><code>tunnels_detail</code> — per-tunnel: <code>remoteAddress</code>, <code>streamCount</code>, <code>bytesIn</code>, <code>bytesOut</code>, <code>agentVersion</code></li>
305
+ </ul>
264
306
 
265
307
  <h3>Runtime Behavior</h3>
266
308
  <ul>
267
309
  <li>Status will show <code>listening on port [port]</code> when active.</li>
268
310
  <li>Each client connection updates the status with the current number of connected clients.</li>
269
311
  <li>All client connections are properly closed when the node is stopped or removed.</li>
312
+ <li>The output emits a message for each tunnel that successfully registers (after CONFIG handshake).</li>
313
+ <li>If a WebSocket connects but does not complete the CONFIG handshake, no events are emitted.</li>
314
+ <li>Any message sent to the input triggers a metrics snapshot on the output with <code>event: "metrics"</code>.</li>
270
315
  </ul>
271
316
 
272
317
  <h3>Logging</h3>
@@ -287,7 +332,9 @@
287
332
  </li>
288
333
  <li>Redeploying the flow will gracefully shut down existing WebSocket servers and restart them with updated configuration.</li>
289
334
  <li>Log level changes require redeployment to take effect.</li>
290
- <li>This node does not have input or output wires it works autonomously based on its configuration.</li>
335
+ <li>The output is optional if not wired, the node behaves exactly as before (status-only monitoring).</li>
336
+ <li>The input accepts any message to trigger a metrics snapshot. Connect an <code>inject</code> node for periodic metrics.</li>
337
+ <li>Events are only emitted for tunnels that complete the CONFIG handshake. Raw WebSocket connections that are rejected or close before registration do not emit events.</li>
291
338
  </ul>
292
339
  </script>
293
340
 
@@ -299,8 +346,8 @@
299
346
  wstunnel: { type: 'wstunnel', required: true },
300
347
  name: { value: '' },
301
348
  },
302
- inputs: 0,
303
- outputs: 0,
349
+ inputs: 1,
350
+ outputs: 1,
304
351
  icon: 'font-awesome/fa-lock',
305
352
  label: function () {
306
353
  return this.name || 'tunnel Websocket server';
@@ -310,3 +357,78 @@
310
357
  },
311
358
  });
312
359
  </script>
360
+
361
+ <script type="text/x-red" data-template-name="wstunnel command">
362
+ <div class="form-row">
363
+ <label for="node-input-wstunnel"><i class="fa fa-gears"></i> Tunnel Config</label>
364
+ <input type="text" id="node-input-wstunnel">
365
+ </div>
366
+ <div class="form-row">
367
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
368
+ <input type="text" id="node-input-name" placeholder="Name">
369
+ </div>
370
+ </script>
371
+
372
+ <script type="text/html" data-help-name="wstunnel command">
373
+ <p>Sends commands to connected tunnel clients via WebSocket.</p>
374
+
375
+ <h3>Inputs</h3>
376
+ <dl class="message-properties">
377
+ <dt>payload <span class="property-type">object</span></dt>
378
+ <dd>The command payload. May contain <code>tunnelId</code>, <code>command</code>, and <code>args</code>.</dd>
379
+
380
+ <dt>tunnelId <span class="property-type">string</span></dt>
381
+ <dd>Alternative to <code>payload.tunnelId</code>. The UUID of the target tunnel.</dd>
382
+
383
+ <dt>command <span class="property-type">string</span></dt>
384
+ <dd>Alternative to <code>payload.command</code>. The command name to send (e.g. <code>"kill"</code>).</dd>
385
+
386
+ <dt>args <span class="property-type">object</span></dt>
387
+ <dd>Alternative to <code>payload.args</code>. Optional arguments for the command.</dd>
388
+ </dl>
389
+
390
+ <h3>Outputs</h3>
391
+ <dl class="message-properties">
392
+ <dt class="optional">output 0 — success</dt>
393
+ <dd>Sent when the command was delivered. <code>msg.payload</code> is <code>true</code>.</dd>
394
+
395
+ <dt class="optional">output 1 — failure</dt>
396
+ <dd>Sent when the tunnel was not found or not connected. <code>msg.payload</code> is <code>false</code>.</dd>
397
+ </dl>
398
+
399
+ <h3>Details</h3>
400
+ <p>
401
+ The node sends a command to a connected tunnel client. The target tunnel is identified by its <code>tunnelId</code>.
402
+ If the tunnel exists and its WebSocket is open, the command is delivered and output 0 fires.
403
+ If the tunnel does not exist or is disconnected, output 1 fires with <code>msg.error</code>.
404
+ </p>
405
+ <p>Missing <code>tunnelId</code> or <code>command</code> sends the message to output 1 with an error.</p>
406
+
407
+ <h3>Example</h3>
408
+ <pre>
409
+ [inject: {tunnelId: "uuid", command: "kill"}]
410
+ → [wstunnel command]
411
+ → output 0 → [debug: success]
412
+ → output 1 → [debug: failure]
413
+ </pre>
414
+ </script>
415
+
416
+ <script type="text/javascript">
417
+ RED.nodes.registerType('wstunnel command', {
418
+ category: 'network',
419
+ color: '#a7c957',
420
+ defaults: {
421
+ wstunnel: { type: 'wstunnel', required: true },
422
+ name: { value: '' },
423
+ },
424
+ inputs: 1,
425
+ outputs: 2,
426
+ icon: 'font-awesome/fa-terminal',
427
+ label: function () {
428
+ return this.name || 'tunnel command';
429
+ },
430
+ labelStyle: function () {
431
+ return this.name ? 'node_label_italic' : '';
432
+ },
433
+ });
434
+ </script>