@sandeepk1729/porter 1.0.1 → 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.
Files changed (2) hide show
  1. package/dist/index.js +915 -65
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ const commander_1 = __nccwpck_require__(909);
15
15
  const package_json_1 = __importDefault(__nccwpck_require__(330));
16
16
  const agent_1 = __nccwpck_require__(257);
17
17
  const config_1 = __nccwpck_require__(750);
18
+ const server_1 = __nccwpck_require__(127);
18
19
  const porter = new commander_1.Command();
19
20
  porter.name("porter").description(package_json_1.default.description).version(package_json_1.default.version); // <-- Dynamically injected
20
21
  // 1. add alias
@@ -22,15 +23,18 @@ porter
22
23
  .command("http")
23
24
  .arguments("<local-port>")
24
25
  .description("Add http port forwarding")
25
- .action(async (localPort) => {
26
+ .option("--ui-port <port>", "Port for the web UI dashboard", "7676")
27
+ .action(async (localPort, options) => {
28
+ const uiPort = parseInt(options.uiPort, 10);
29
+ if (isNaN(uiPort) || uiPort < 1 || uiPort > 65535) {
30
+ console.error(`Invalid --ui-port value: "${options.uiPort}". Must be a number between 1 and 65535.`);
31
+ process.exit(1);
32
+ }
33
+ (0, server_1.startUIServer)(uiPort);
26
34
  console.log(`Connecting to porter server and forwarding to local port ${localPort}`);
27
35
  config_1.caller.request(config_1.REQ_BODY)
28
- .on("response", (res) => {
29
- console.log("Unexpected response from server:", res.statusCode);
30
- res.on("data", (chunk) => {
31
- console.log("Response body:", chunk.toString());
32
- });
33
- })
36
+ .on("error", (err) => console.error(`Connection to porter server failed: ${err.message}\n` +
37
+ `Make sure the porter server is reachable and try again.`))
34
38
  .on("upgrade", (0, agent_1.upgradeHandler)(localPort))
35
39
  .end();
36
40
  });
@@ -98,6 +102,728 @@ if (!process.argv.slice(2).length) {
98
102
  command_1.default.parse(process.argv);
99
103
 
100
104
 
105
+ /***/ }),
106
+
107
+ /***/ 418:
108
+ /***/ ((__unused_webpack_module, exports) => {
109
+
110
+ "use strict";
111
+
112
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
113
+ class Channel {
114
+ clients;
115
+ /// Constructor
116
+ constructor() {
117
+ this.clients = new Set();
118
+ }
119
+ subscribe = (res) => {
120
+ this.clients.add(res);
121
+ };
122
+ unsubscribe(res) {
123
+ this.clients.delete(res);
124
+ }
125
+ broadcast = (event, html) => {
126
+ const payload = Array.isArray(html) ? html.join("\n") : html;
127
+ for (const client of this.clients) {
128
+ this.send(client, event, payload);
129
+ }
130
+ };
131
+ /**
132
+ * Write a single SSE message. Multi-line HTML is split into multiple
133
+ * `data:` lines so no double-newline accidentally terminates the frame.
134
+ */
135
+ send = (client, event, html) => {
136
+ const safe = html.trim().replace(/\n{2,}/g, "\n");
137
+ const dataLines = safe
138
+ .split("\n")
139
+ .map((l) => `data: ${l}`)
140
+ .join("\n");
141
+ client.write(`event: ${event}\n${dataLines}\n\n`);
142
+ };
143
+ }
144
+ exports["default"] = Channel;
145
+
146
+
147
+ /***/ }),
148
+
149
+ /***/ 265:
150
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
151
+
152
+ "use strict";
153
+
154
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
155
+ exports.agentEvents = void 0;
156
+ const node_events_1 = __nccwpck_require__(474);
157
+ const agentEvents = new node_events_1.EventEmitter();
158
+ exports.agentEvents = agentEvents;
159
+ agentEvents.setMaxListeners(100);
160
+
161
+
162
+ /***/ }),
163
+
164
+ /***/ 823:
165
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
166
+
167
+ "use strict";
168
+
169
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
170
+ exports.INDEX_HTML = exports.EMPTY_DETAIL_HTML = exports.EMPTY_MSG_HTML = void 0;
171
+ exports.rowHtml = rowHtml;
172
+ exports.countOobHtml = countOobHtml;
173
+ exports.detailHtml = detailHtml;
174
+ exports.detailInnerHtml = detailInnerHtml;
175
+ const utils_1 = __nccwpck_require__(448);
176
+ /**
177
+ * A single request row. If `oobSpec` is provided the element gets
178
+ * `hx-swap-oob` so HTMX applies it as an out-of-band DOM patch.
179
+ *
180
+ * Click behaviour is handled by a delegated listener on #list-panel
181
+ * (see the inline script at the bottom of the page) so that it works
182
+ * correctly even when rows are continuously added/replaced via SSE OOB
183
+ * swaps – per-element hx-* attributes would require htmx.process() to
184
+ * be called after every OOB swap, which the SSE extension does not
185
+ * guarantee.
186
+ */
187
+ function rowHtml(r, oobSpec) {
188
+ const dur = r.endTime ? `${r.endTime - r.startTime}ms` : "&hellip;";
189
+ const stHtml = r.responseStatus
190
+ ? `<div class="st ${(0, utils_1.stClass)(r.responseStatus)}">${r.responseStatus}</div>`
191
+ : `<div class="st st-p">&hellip;</div>`;
192
+ const oob = oobSpec ? ` hx-swap-oob="${(0, utils_1.esc)(oobSpec)}"` : "";
193
+ const cls = `req-row${r.done ? "" : " pending"}`;
194
+ return (`<div ${oob}>
195
+ <div id="row-${r.requestId}" class="${cls}">
196
+ <span class="mth ${(0, utils_1.mthClass)(r.method)}">${(0, utils_1.esc)(r.method)}</span>
197
+ <span class="req-path" title="${(0, utils_1.esc)(r.path)}">${(0, utils_1.esc)(r.path)}</span>
198
+ <div class="req-meta">
199
+ ${stHtml}
200
+ <div class="dur">${dur}</div>
201
+ </div>
202
+ </div>
203
+ </div>`);
204
+ }
205
+ /** Request count badge, returned as an OOB outerHTML swap. */
206
+ function countOobHtml(n) {
207
+ return `<span id="req-count" hx-swap-oob="outerHTML">${n} ${n === 1 ? "request" : "requests"}</span>`;
208
+ }
209
+ /** Empty-state placeholder for #list-panel. */
210
+ const EMPTY_MSG_HTML = `<div id="empty-msg" class="empty-list">` +
211
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">` +
212
+ `<path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>` +
213
+ `</svg><span>Waiting for requests&hellip;</span></div>`;
214
+ exports.EMPTY_MSG_HTML = EMPTY_MSG_HTML;
215
+ /** Empty-state for #detail-panel (used by the clear response OOB). */
216
+ const EMPTY_DETAIL_HTML = `<div id="detail-panel" hx-swap-oob="outerHTML" class="center">` +
217
+ `<div class="ph">` +
218
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">` +
219
+ `<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>` +
220
+ `</svg><span>Select a request to view details</span></div></div>`;
221
+ exports.EMPTY_DETAIL_HTML = EMPTY_DETAIL_HTML;
222
+ /**
223
+ * Inner HTML of the detail panel for request `r`.
224
+ * Served both by `GET /request/:id` and as the payload of
225
+ * `response-end-{id}` SSE events (the `.detail-content` wrapper
226
+ * already has `sse-swap="response-end-{id}"` so HTMX replaces
227
+ * its innerHTML automatically when that event arrives).
228
+ */
229
+ function detailInnerHtml(r) {
230
+ const dur = r.endTime ? `${r.endTime - r.startTime} ms` : "pending&hellip;";
231
+ const stHtml = r.responseStatus
232
+ ? `<span class="st ${(0, utils_1.stClass)(r.responseStatus)}">${r.responseStatus}</span>`
233
+ : `<span class="st st-p">Pending&hellip;</span>`;
234
+ let html = `<div class="summary">` +
235
+ `<span class="mth ${(0, utils_1.mthClass)(r.method)}">${(0, utils_1.esc)(r.method)}</span>` +
236
+ `<span class="s-path">${(0, utils_1.esc)(r.path)}</span>` +
237
+ `${stHtml}<span class="s-dur">${dur}</span>` +
238
+ `</div>` +
239
+ `<div class="section"><div class="section-hdr">Request Headers</div>` +
240
+ `${(0, utils_1.headersHtml)(r.reqHeaders)}</div>` +
241
+ `<div class="section"><div class="section-hdr">Request Body</div>` +
242
+ `${(0, utils_1.bodyHtml)(r.reqBodyChunks)}</div>`;
243
+ if (r.responseStatus !== null) {
244
+ html +=
245
+ `<div class="section"><div class="section-hdr">Response Headers</div>` +
246
+ `${(0, utils_1.headersHtml)(r.resHeaders)}</div>` +
247
+ `<div class="section"><div class="section-hdr">Response Body</div>` +
248
+ `${(0, utils_1.bodyHtml)(r.resBodyChunks)}</div>`;
249
+ }
250
+ return html;
251
+ }
252
+ /**
253
+ * Full detail panel content for `GET /request/:id`.
254
+ * The `.detail-content` wrapper carries `sse-swap="response-end-{id}"`
255
+ * so HTMX's SSE extension auto-updates the detail panel when the
256
+ * response completes without any client-side JavaScript.
257
+ */
258
+ function detailHtml(r) {
259
+ return (`<div class="detail-content"` +
260
+ ` sse-swap="response-end-${r.requestId}"` +
261
+ ` hx-swap="innerHTML">` +
262
+ detailInnerHtml(r) +
263
+ `</div>`);
264
+ }
265
+ // ── Embedded HTML dashboard ───────────────────────────────────────────────────
266
+ //
267
+ // Technology choices:
268
+ // HTMX (htmx.org) – SSE live-swap via hx-ext="sse", REST actions via
269
+ // hx-get / hx-delete, out-of-band (OOB) DOM patches from the server.
270
+ // htmx-ext-sse – official SSE extension for HTMX.
271
+ // Hyperscript (_hyperscript.org) – declarative DOM interactions via `_=`
272
+ // attributes: active-row selection, connection status dot, no JS block.
273
+ //
274
+ // All three libraries are loaded from unpkg CDN via <script> tags so the
275
+ // dashboard works without any bundled assets or local file serving.
276
+ //
277
+ // Key patterns:
278
+ // • <body hx-ext="sse" sse-connect="/events"> – body is the SSE root.
279
+ // • #sse-sink (hidden) absorbs `ui-update` SSE events; OOB fragments in
280
+ // those events patch #list-panel rows, #req-count, #empty-msg in-place.
281
+ // • A delegated click listener on #list-panel uses htmx.ajax() to fetch
282
+ // server-rendered detail HTML into #detail-panel when a row is clicked.
283
+ // Using delegation rather than per-row hx-get/hx-trigger avoids the
284
+ // need for htmx.process() to be called after every SSE OOB swap.
285
+ // • The .detail-content wrapper returned by GET /request/:id carries
286
+ // sse-swap="response-end-{id}" so HTMX auto-refreshes the detail panel
287
+ // when that per-request SSE event fires – zero client JS needed.
288
+ // • Clear button uses hx-delete="/requests"; the server response carries
289
+ // OOB patches to reset #detail-panel and #req-count.
290
+ // ─────────────────────────────────────────────────────────────────────────────
291
+ const INDEX_HTML = `<!DOCTYPE html>
292
+ <html lang="en">
293
+ <head>
294
+ <meta charset="UTF-8">
295
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
296
+ <title>Porter Agent \u2014 Live Traffic</title>
297
+ <script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"
298
+ integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
299
+ crossorigin="anonymous"><\/script>
300
+ <script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"
301
+ integrity="sha384-fw+eTlCc7suMV/1w/7fr2/PmwElUIt5i82bi+qTiLXvjRXZ2/FkiTNA/w0MhXnGI"
302
+ crossorigin="anonymous"><\/script>
303
+ <script src="https://unpkg.com/hyperscript.org@0.9.13/dist/_hyperscript.min.js"
304
+ integrity="sha384-5yQ5JTatiFEgeiEB4mfkRI3oTGtaNpbJGdcciZ4IEYFpLGt8yDsGAd7tKiMwnX9b"
305
+ crossorigin="anonymous"><\/script>
306
+ <style>
307
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
308
+ body{font-family:'Segoe UI',system-ui,sans-serif;background:#0f172a;color:#e2e8f0;height:100vh;display:flex;flex-direction:column;overflow:hidden}
309
+ header{background:#1e293b;padding:12px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #334155;flex-shrink:0}
310
+ .logo{font-size:1.1rem;font-weight:700;color:#38bdf8;letter-spacing:-0.3px}
311
+ .conn-status{display:flex;align-items:center;gap:8px;font-size:0.8125rem;color:#94a3b8}
312
+ .dot{width:8px;height:8px;border-radius:50%;background:#22c55e}
313
+ .dot.off{background:#ef4444}
314
+ @keyframes blink{0%,100%{opacity:1}50%{opacity:0.4}}
315
+ .dot.live{animation:blink 1.8s ease-in-out infinite}
316
+ .toolbar{background:#1e293b;padding:6px 20px;display:flex;align-items:center;gap:10px;border-bottom:1px solid #334155;flex-shrink:0}
317
+ .btn{padding:5px 12px;border-radius:5px;border:1px solid #475569;background:transparent;color:#94a3b8;cursor:pointer;font-size:0.8rem;transition:all .15s}
318
+ .btn:hover{background:#334155;color:#e2e8f0}
319
+ #req-count{margin-left:auto;font-size:0.8rem;color:#475569}
320
+ main{display:flex;flex:1;overflow:hidden}
321
+ #list-panel{width:360px;min-width:240px;overflow-y:auto;border-right:1px solid #334155;flex-shrink:0}
322
+ .empty-list{display:flex;flex-direction:column;align-items:center;justify-content:center;height:200px;gap:10px;color:#475569;font-size:0.875rem;padding:20px;text-align:center}
323
+ .req-row{padding:8px 14px;border-bottom:1px solid #1e293b;cursor:pointer;display:grid;grid-template-columns:52px 1fr auto;gap:8px;align-items:center;transition:background .1s}
324
+ .req-row:hover{background:#1a2744}
325
+ .req-row.active{background:#1e3a5f;border-left:3px solid #38bdf8;padding-left:11px}
326
+ .req-row.pending{opacity:.75}
327
+ .mth{font-size:.65rem;font-weight:800;padding:2px 5px;border-radius:4px;text-align:center;letter-spacing:.4px;white-space:nowrap}
328
+ .mth-GET{background:#0c4a6e;color:#38bdf8}
329
+ .mth-POST{background:#14532d;color:#4ade80}
330
+ .mth-PUT{background:#78350f;color:#fbbf24}
331
+ .mth-PATCH{background:#4c1d95;color:#a78bfa}
332
+ .mth-DELETE{background:#7f1d1d;color:#f87171}
333
+ .mth-HEAD,.mth-OPTIONS{background:#1e3a5f;color:#93c5fd}
334
+ .mth-other{background:#1e293b;color:#94a3b8}
335
+ .req-path{font-size:.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#cbd5e1}
336
+ .req-meta{text-align:right;white-space:nowrap}
337
+ .st{font-size:.75rem;font-weight:600}
338
+ .st-2{color:#4ade80}.st-3{color:#60a5fa}.st-4{color:#fbbf24}.st-5{color:#f87171}.st-p{color:#64748b}
339
+ .dur{font-size:.7rem;color:#475569;margin-top:2px}
340
+ #detail-panel{flex:1;overflow-y:auto;padding:16px 20px}
341
+ #detail-panel.center{display:flex;align-items:center;justify-content:center}
342
+ .ph{display:flex;flex-direction:column;align-items:center;gap:10px;color:#475569;font-size:.9rem}
343
+ .summary{display:flex;align-items:center;gap:10px;background:#1e293b;padding:10px 14px;border-radius:8px;margin-bottom:16px;font-size:.875rem;flex-wrap:wrap}
344
+ .summary .s-path{color:#94a3b8;flex:1;word-break:break-all;font-family:monospace;font-size:.8rem}
345
+ .summary .s-dur{font-size:.75rem;color:#64748b}
346
+ .section{margin-bottom:18px}
347
+ .section-hdr{font-size:.75rem;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px;display:flex;align-items:center;gap:8px}
348
+ .section-hdr::after{content:"";flex:1;height:1px;background:#334155}
349
+ .hdr-tbl{width:100%;border-collapse:collapse;font-size:.78rem}
350
+ .hdr-tbl tr:nth-child(odd) td{background:#111827}
351
+ .hdr-tbl td{padding:4px 8px;vertical-align:top;word-break:break-word}
352
+ .hdr-tbl td:first-child{color:#7dd3fc;font-family:monospace;white-space:nowrap;width:36%;padding-right:12px}
353
+ .hdr-tbl td:last-child{color:#e2e8f0;font-family:monospace}
354
+ .body-box{background:#111827;border:1px solid #334155;border-radius:6px}
355
+ .body-pre{padding:10px 12px;font-family:'Cascadia Code',Consolas,monospace;font-size:.78rem;line-height:1.65;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow-y:auto;color:#e2e8f0}
356
+ .body-none{padding:10px 12px;color:#475569;font-style:italic;font-size:.8rem}
357
+ .no-data{color:#475569;font-size:.8rem;font-style:italic}
358
+ ::-webkit-scrollbar{width:5px;height:5px}
359
+ ::-webkit-scrollbar-track{background:#0f172a}
360
+ ::-webkit-scrollbar-thumb{background:#334155;border-radius:3px}
361
+ ::-webkit-scrollbar-thumb:hover{background:#475569}
362
+ </style>
363
+ </head>
364
+ <!--
365
+ hx-ext="sse" – enable the HTMX SSE extension on the whole page
366
+ sse-connect="/events" – open an EventSource to our /events endpoint
367
+ _="..." – Hyperscript: update the connection-status dot/label
368
+ when HTMX fires htmx:sseOpen / htmx:sseError on body
369
+ -->
370
+ <body hx-ext="sse" sse-connect="/events"
371
+ _="on htmx:sseOpen
372
+ remove .off from #dot
373
+ add .live to #dot
374
+ set the textContent of #conn-label to 'Connected'
375
+ on htmx:sseError
376
+ remove .live from #dot
377
+ add .off to #dot
378
+ set the textContent of #conn-label to 'Disconnected \u2014 retrying\u2026'">
379
+
380
+ <header>
381
+ <div class="logo">&#9889; Porter Agent &mdash; Live Traffic</div>
382
+ <div class="conn-status">
383
+ <div class="dot off" id="dot"></div>
384
+ <span id="conn-label">Connecting&hellip;</span>
385
+ </div>
386
+ </header>
387
+
388
+ <div class="toolbar">
389
+ <!--
390
+ hx-delete="/requests" – DELETE /requests clears server state
391
+ hx-target / hx-swap – main response goes into #list-panel innerHTML
392
+ The server response also carries OOB patches for #detail-panel and
393
+ #req-count so everything resets in one round-trip, no JS needed.
394
+ -->
395
+ <button class="btn"
396
+ hx-delete="/requests"
397
+ hx-target="#list-panel"
398
+ hx-swap="innerHTML">Clear</button>
399
+ <span id="req-count">0 requests</span>
400
+ </div>
401
+
402
+ <main>
403
+ <div id="list-panel">
404
+ <div class="empty-list" id="empty-msg">
405
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
406
+ <span>Waiting for requests&hellip;</span>
407
+ </div>
408
+ </div>
409
+ <div id="detail-panel" class="center">
410
+ <div class="ph">
411
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
412
+ <span>Select a request to view details</span>
413
+ </div>
414
+ </div>
415
+ </main>
416
+
417
+ <!--
418
+ Hidden SSE sink: absorbs "ui-update" events from the server.
419
+ Each "ui-update" message body carries one or more hx-swap-oob fragments
420
+ that HTMX applies directly to matching elements in the DOM
421
+ (new rows, count badge, row status patches, etc.).
422
+ -->
423
+ <div id="sse-sink" sse-swap="ui-update" hx-swap="innerHTML" style="display:none"></div>
424
+
425
+ <!--
426
+ Row click delegation
427
+ ───────────────────
428
+ Rows are injected/replaced continuously via SSE OOB swaps. The SSE
429
+ extension does not guarantee that htmx.process() is called on every
430
+ new/replaced element, so per-element hx-get/hx-trigger attributes
431
+ are unreliable. A single delegated listener on the stable #list-panel
432
+ container works regardless of how many times rows are swapped.
433
+ -->
434
+ <script>
435
+ (function () {
436
+ document.getElementById('list-panel').addEventListener('click', function (e) {
437
+ var row = e.target.closest('[id^="row-"]');
438
+ if (!row) return;
439
+ document.querySelectorAll('.req-row').forEach(function (r) { r.classList.remove('active'); });
440
+ row.classList.add('active');
441
+ document.getElementById('detail-panel').classList.remove('center');
442
+ htmx.ajax('GET', '/request/' + row.id.replace(/^row-/, ''), {
443
+ target: '#detail-panel',
444
+ swap: 'innerHTML',
445
+ });
446
+ });
447
+ }());
448
+ <\/script>
449
+
450
+ </body>
451
+ </html>`;
452
+ exports.INDEX_HTML = INDEX_HTML;
453
+
454
+
455
+ /***/ }),
456
+
457
+ /***/ 366:
458
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
459
+
460
+ "use strict";
461
+
462
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
463
+ if (k2 === undefined) k2 = k;
464
+ var desc = Object.getOwnPropertyDescriptor(m, k);
465
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
466
+ desc = { enumerable: true, get: function() { return m[k]; } };
467
+ }
468
+ Object.defineProperty(o, k2, desc);
469
+ }) : (function(o, m, k, k2) {
470
+ if (k2 === undefined) k2 = k;
471
+ o[k2] = m[k];
472
+ }));
473
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
474
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
475
+ };
476
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
477
+ __exportStar(__nccwpck_require__(823), exports);
478
+
479
+
480
+ /***/ }),
481
+
482
+ /***/ 448:
483
+ /***/ ((__unused_webpack_module, exports) => {
484
+
485
+ "use strict";
486
+
487
+ // ── HTML rendering helpers ────────────────────────────────────────────────────
488
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
489
+ exports.bodyHtml = exports.headersHtml = exports.stClass = exports.mthClass = exports.esc = void 0;
490
+ const esc = (s) => {
491
+ return String(s ?? "")
492
+ .replace(/&/g, "&amp;")
493
+ .replace(/</g, "&lt;")
494
+ .replace(/>/g, "&gt;")
495
+ .replace(/"/g, "&quot;")
496
+ .replace(/'/g, "&#x27;");
497
+ };
498
+ exports.esc = esc;
499
+ const mthClass = (m) => {
500
+ return ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].includes(m)
501
+ ? `mth-${m}`
502
+ : "mth-other";
503
+ };
504
+ exports.mthClass = mthClass;
505
+ const stClass = (s) => {
506
+ if (!s)
507
+ return "st-p";
508
+ if (s >= 500)
509
+ return "st-5";
510
+ if (s >= 400)
511
+ return "st-4";
512
+ if (s >= 300)
513
+ return "st-3";
514
+ return "st-2";
515
+ };
516
+ exports.stClass = stClass;
517
+ const decodeBodyChunks = (chunks) => {
518
+ if (!chunks.length)
519
+ return "";
520
+ try {
521
+ return chunks
522
+ .map((c) => Buffer.from(c, "base64").toString("utf8"))
523
+ .join("");
524
+ }
525
+ catch {
526
+ return chunks.join("");
527
+ }
528
+ };
529
+ const prettyBody = (raw) => {
530
+ try {
531
+ return JSON.stringify(JSON.parse(raw), null, 2);
532
+ }
533
+ catch {
534
+ return raw;
535
+ }
536
+ };
537
+ const headersHtml = (headers) => {
538
+ const entries = Object.entries(headers ?? {});
539
+ if (!entries.length) {
540
+ return `<span class="no-data">No headers</span>`;
541
+ }
542
+ const rows = entries
543
+ .map(([k, v]) => `<tr><td>${esc(k)}</td><td>${esc(String(v))}</td></tr>`)
544
+ .join("");
545
+ return `<table class="hdr-tbl"><tbody>${rows}</tbody></table>`;
546
+ };
547
+ exports.headersHtml = headersHtml;
548
+ const bodyHtml = (chunks) => {
549
+ const raw = decodeBodyChunks(chunks);
550
+ if (!raw) {
551
+ return `<div class="body-box"><div class="body-none">No body</div></div>`;
552
+ }
553
+ return `<div class="body-box"><pre class="body-pre">${esc(prettyBody(raw))}</pre></div>`;
554
+ };
555
+ exports.bodyHtml = bodyHtml;
556
+
557
+
558
+ /***/ }),
559
+
560
+ /***/ 127:
561
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
562
+
563
+ "use strict";
564
+
565
+ var __importDefault = (this && this.__importDefault) || function (mod) {
566
+ return (mod && mod.__esModule) ? mod : { "default": mod };
567
+ };
568
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
569
+ exports.startUIServer = startUIServer;
570
+ const node_http_1 = __importDefault(__nccwpck_require__(67));
571
+ const events_1 = __nccwpck_require__(265);
572
+ const storage_1 = __importDefault(__nccwpck_require__(892));
573
+ const types_1 = __nccwpck_require__(888);
574
+ const channel_1 = __importDefault(__nccwpck_require__(418));
575
+ const buffer_1 = __nccwpck_require__(88);
576
+ const html_1 = __nccwpck_require__(366);
577
+ const channel = new channel_1.default();
578
+ const records = new storage_1.default();
579
+ const processEvent = (event, data) => {
580
+ const channelEvents = [];
581
+ switch (event) {
582
+ case "request-start": {
583
+ const record = new types_1.RequestRecord(data);
584
+ records.set(record.requestId, record);
585
+ channelEvents.push({
586
+ event: "ui-update",
587
+ fragments: [
588
+ // Prepend the new row into #list-panel
589
+ (0, html_1.rowHtml)(record, "afterbegin:#list-panel"),
590
+ // Update the request count badge
591
+ (0, html_1.countOobHtml)(records.size()),
592
+ ],
593
+ });
594
+ // On the very first request hide the "waiting" placeholder
595
+ if (records.size() === 1) {
596
+ channelEvents.push({
597
+ event: "ui-update",
598
+ fragments: [
599
+ `<div id="empty-msg" hx-swap-oob="outerHTML" style="display:none"></div>`,
600
+ ],
601
+ });
602
+ }
603
+ break;
604
+ }
605
+ case "request-data": {
606
+ records.get(data.requestId)?.addRequestBody(data);
607
+ break;
608
+ }
609
+ case "request-end": {
610
+ // Nothing to broadcast; detail updates happen on response-end
611
+ break;
612
+ }
613
+ case "response-start": {
614
+ const r = records.get(data.requestId);
615
+ if (!r)
616
+ break;
617
+ r.setResponseStart(data);
618
+ // Patch the row in-place (status badge update)
619
+ channelEvents.push({
620
+ event: "ui-update",
621
+ fragments: [(0, html_1.rowHtml)(r, `outerHTML:#row-${r.requestId}`)],
622
+ });
623
+ break;
624
+ }
625
+ case "response-data": {
626
+ records.get(data.requestId)?.addResponseBody(data);
627
+ break;
628
+ }
629
+ case "response-end": {
630
+ const r = records.get(data.requestId);
631
+ if (!r)
632
+ break;
633
+ r.setResponseEnd(data);
634
+ // Patch the row (timing + remove .pending)
635
+ channelEvents.push({
636
+ event: "ui-update",
637
+ fragments: [(0, html_1.rowHtml)(r, `outerHTML:#row-${r.requestId}`)],
638
+ });
639
+ // Push updated detail content to any open detail panel for this request
640
+ channelEvents.push({
641
+ event: `response-end-${r.requestId}`,
642
+ fragments: [(0, html_1.detailHtml)(r)],
643
+ });
644
+ break;
645
+ }
646
+ }
647
+ channelEvents.forEach(({ event, fragments }) => {
648
+ channel.broadcast(event, fragments);
649
+ });
650
+ };
651
+ Object.values(buffer_1.EventType).forEach((event) => {
652
+ events_1.agentEvents.on(event, (data) => processEvent(event, data));
653
+ });
654
+ // ── HTTP server ───────────────────────────────────────────────────────────────
655
+ function startUIServer(port = 7676) {
656
+ const server = node_http_1.default.createServer((req, res) => {
657
+ const url = req.url ?? "/";
658
+ // ── GET /events (SSE stream) ─────────────────────────────────────────
659
+ if (url === "/events") {
660
+ res.writeHead(200, {
661
+ "Content-Type": "text/event-stream",
662
+ "Cache-Control": "no-cache",
663
+ Connection: "keep-alive",
664
+ "Access-Control-Allow-Origin": "*",
665
+ });
666
+ res.write(": connected\n\n");
667
+ // Replay current state so late-joining browsers see existing traffic
668
+ if (records.hasData()) {
669
+ const rows = records.getValues()
670
+ .reverse()
671
+ .map((r) => (0, html_1.rowHtml)(r))
672
+ .join("\n");
673
+ channel.send(res, "ui-update", [
674
+ `<div id="list-panel" hx-swap-oob="innerHTML">${rows}</div>`,
675
+ (0, html_1.countOobHtml)(records.size()),
676
+ `<div id="empty-msg" hx-swap-oob="outerHTML" style="display:none"></div>`,
677
+ ].join("\n"));
678
+ }
679
+ channel.subscribe(res);
680
+ req.on("close", () => channel.unsubscribe(res));
681
+ return;
682
+ }
683
+ // ── GET /request/:id (detail panel fragment) ─────────────────────────
684
+ const detailMatch = url.match(/^\/request\/([a-f0-9]+)$/);
685
+ if (req.method === "GET" && detailMatch) {
686
+ const r = records.get(detailMatch[1] ?? "");
687
+ if (!r) {
688
+ res.writeHead(404);
689
+ res.end();
690
+ return;
691
+ }
692
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
693
+ res.end((0, html_1.detailHtml)(r));
694
+ return;
695
+ }
696
+ // ── DELETE /requests (clear all) ────────────────────────────────────
697
+ if (req.method === "DELETE" && url === "/requests") {
698
+ records.clear();
699
+ // Main response → replaces #list-panel innerHTML (hx-target on button)
700
+ // OOB responses → reset #detail-panel and #req-count in the same trip
701
+ const body = html_1.EMPTY_MSG_HTML +
702
+ html_1.EMPTY_DETAIL_HTML +
703
+ `<span id="req-count" hx-swap-oob="outerHTML">0 requests</span>`;
704
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
705
+ res.end(body);
706
+ return;
707
+ }
708
+ // ── GET / (main page) ────────────────────────────────────────────────
709
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
710
+ res.end(html_1.INDEX_HTML);
711
+ });
712
+ server.on("error", (err) => {
713
+ if (err.code === "EADDRINUSE") {
714
+ console.error(`⚠️ Web UI port ${port} is already in use. UI will not be available.`);
715
+ }
716
+ else {
717
+ console.error("Web UI server error:", err.message);
718
+ }
719
+ });
720
+ server.listen(port, "127.0.0.1", () => {
721
+ console.log(`\uD83C\uDF10 Web UI available at http://localhost:${port}`);
722
+ });
723
+ return server;
724
+ }
725
+
726
+
727
+ /***/ }),
728
+
729
+ /***/ 892:
730
+ /***/ ((__unused_webpack_module, exports) => {
731
+
732
+ "use strict";
733
+
734
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
735
+ class Storage {
736
+ store;
737
+ constructor() {
738
+ this.store = new Map();
739
+ }
740
+ size = () => {
741
+ return this.store.size;
742
+ };
743
+ hasData = () => {
744
+ return this.store.size > 0;
745
+ };
746
+ has = (key) => {
747
+ return this.store.has(key);
748
+ };
749
+ get = (key) => {
750
+ return this.store.get(key);
751
+ };
752
+ getValues = () => {
753
+ return Array.from(this.store.values());
754
+ };
755
+ set = (key, value) => {
756
+ this.store.set(key, value);
757
+ };
758
+ delete = (key) => {
759
+ this.store.delete(key);
760
+ };
761
+ clear = () => {
762
+ this.store.clear();
763
+ };
764
+ }
765
+ exports["default"] = Storage;
766
+
767
+
768
+ /***/ }),
769
+
770
+ /***/ 888:
771
+ /***/ ((__unused_webpack_module, exports) => {
772
+
773
+ "use strict";
774
+
775
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
776
+ exports.RequestRecord = void 0;
777
+ class RequestRecord {
778
+ requestId;
779
+ method;
780
+ path;
781
+ reqHeaders;
782
+ reqBodyChunks; // base64 encoded chunks
783
+ responseStatus;
784
+ resHeaders;
785
+ resBodyChunks; // base64 encoded chunks
786
+ startTime;
787
+ endTime;
788
+ done;
789
+ constructor({ requestId, payload, timestamp }) {
790
+ this.requestId = requestId;
791
+ this.method = payload.method;
792
+ this.path = payload.path;
793
+ this.reqHeaders = payload.headers;
794
+ this.startTime = timestamp;
795
+ this.reqBodyChunks = [];
796
+ this.resHeaders = {};
797
+ this.resBodyChunks = [];
798
+ this.responseStatus = null;
799
+ this.endTime = null;
800
+ this.done = false;
801
+ }
802
+ log(x) {
803
+ console.log(`[${this.requestId}]`, x, 'json:', JSON.stringify(x));
804
+ }
805
+ addRequestBody = ({ payload }) => {
806
+ if (!payload)
807
+ return;
808
+ this.reqBodyChunks.push(payload);
809
+ };
810
+ addResponseBody = ({ payload }) => {
811
+ if (!payload)
812
+ return;
813
+ this.resBodyChunks.push(payload);
814
+ };
815
+ setResponseStart = ({ payload }) => {
816
+ this.responseStatus = payload?.status;
817
+ this.resHeaders = payload?.headers || {};
818
+ };
819
+ setResponseEnd = ({ timestamp }) => {
820
+ this.endTime = timestamp;
821
+ this.done = true;
822
+ };
823
+ }
824
+ exports.RequestRecord = RequestRecord;
825
+
826
+
101
827
  /***/ }),
102
828
 
103
829
  /***/ 257:
@@ -113,6 +839,8 @@ exports.upgradeHandler = void 0;
113
839
  const node_http_1 = __importDefault(__nccwpck_require__(67));
114
840
  const buffer_1 = __nccwpck_require__(88);
115
841
  const config_1 = __nccwpck_require__(750);
842
+ const events_1 = __nccwpck_require__(265);
843
+ const requests = new Map();
116
844
  const upgradeHandler = (localPort) => (res, socket) => {
117
845
  let tunnelId = null;
118
846
  let buffer = Buffer.alloc(0);
@@ -127,51 +855,126 @@ const upgradeHandler = (localPort) => (res, socket) => {
127
855
  const { frames, remaining } = (0, buffer_1.decodeFrames)(Buffer.concat([buffer, chunk]));
128
856
  buffer = remaining;
129
857
  frames.forEach((frame) => {
130
- if (frame.type !== buffer_1.FrameType.REQUEST)
858
+ if (frame.type < buffer_1.FrameType.REQUEST_START ||
859
+ frame.type > buffer_1.FrameType.REQUEST_END)
131
860
  return;
132
- const options = {
133
- host: "localhost",
134
- port: parseInt(localPort, 10),
135
- method: frame.payload.method,
136
- path: frame.payload.path,
137
- headers: frame.payload.headers,
138
- };
139
- console.log(`➡️ Incoming request for tunnel ${tunnelId}: `, options);
140
- // Using ANSI escape codes
141
- console.log(`- \x1b[32m${options.method}\x1b[0m \x1b[34m${options.path}\x1b[0m`);
142
- const proxy = node_http_1.default.request(options, (res) => {
143
- let body = "";
144
- res.on("data", (c) => (body += c));
145
- res.on("end", () => socket.write((0, buffer_1.encodeFrame)({
146
- type: buffer_1.FrameType.RESPONSE,
861
+ if (frame.type === buffer_1.FrameType.REQUEST_START) {
862
+ const options = {
863
+ host: "localhost",
864
+ port: parseInt(localPort, 10),
865
+ method: frame.payload.method,
866
+ path: frame.payload.path,
867
+ headers: sanitizeHeaders(frame.payload.headers, localPort),
868
+ };
869
+ // console.log(`➡️ Incoming request for tunnel ${tunnelId}: `, options);
870
+ // Using ANSI escape codes
871
+ console.log(`- \x1b[32m${options.method}\x1b[0m \x1b[34m${options.path}\x1b[0m`);
872
+ tunnelFrame({
147
873
  requestId: frame.requestId,
148
- payload: {
149
- status: res.statusCode,
150
- headers: res.headers,
151
- body,
152
- },
153
- })));
154
- });
155
- proxy.on("error", (err) => socket.write((0, buffer_1.encodeFrame)({
156
- type: buffer_1.FrameType.RESPONSE,
157
- requestId: frame.requestId,
158
- payload: {
159
- status: 502,
160
- headers: {},
161
- body: "Bad Gateway: " + err.message
162
- },
163
- })));
164
- proxy.end();
874
+ type: buffer_1.FrameType.REQUEST_START,
875
+ payload: frame.payload,
876
+ });
877
+ const proxy = node_http_1.default.request(options, (res) => {
878
+ // send response start
879
+ tunnelFrame({
880
+ requestId: frame.requestId,
881
+ type: buffer_1.FrameType.RESPONSE_START,
882
+ payload: {
883
+ status: res.statusCode || 500,
884
+ headers: res.headers,
885
+ },
886
+ });
887
+ // pipe response data
888
+ res.on("data", (c) => {
889
+ // send response data
890
+ tunnelFrame({
891
+ requestId: frame.requestId,
892
+ type: buffer_1.FrameType.RESPONSE_DATA,
893
+ payload: c,
894
+ });
895
+ });
896
+ // response end
897
+ res.on("end", () => {
898
+ tunnelFrame({
899
+ requestId: frame.requestId,
900
+ type: buffer_1.FrameType.RESPONSE_END,
901
+ });
902
+ });
903
+ });
904
+ proxy.on("error", (err) => {
905
+ tunnelFrame({
906
+ requestId: frame.requestId,
907
+ type: buffer_1.FrameType.RESPONSE_START,
908
+ payload: {
909
+ status: 502,
910
+ headers: {},
911
+ body: "Bad Gateway: " + err.message,
912
+ },
913
+ });
914
+ tunnelFrame({
915
+ requestId: frame.requestId,
916
+ type: buffer_1.FrameType.RESPONSE_END,
917
+ });
918
+ });
919
+ requests.set(frame.requestId, proxy);
920
+ }
921
+ else if (frame.type === buffer_1.FrameType.REQUEST_DATA) {
922
+ tunnelFrame({
923
+ requestId: frame.requestId,
924
+ type: buffer_1.FrameType.REQUEST_DATA,
925
+ payload: frame.payload,
926
+ });
927
+ requests.get(frame.requestId)?.write(frame.payload);
928
+ }
929
+ else if (frame.type === buffer_1.FrameType.REQUEST_END) {
930
+ tunnelFrame({
931
+ requestId: frame.requestId,
932
+ type: buffer_1.FrameType.REQUEST_END,
933
+ });
934
+ requests.get(frame.requestId)?.end();
935
+ requests.delete(frame.requestId);
936
+ }
165
937
  });
166
938
  });
167
939
  socket.on("error", (err) => {
168
940
  console.error("Socket error:", err);
169
941
  });
170
942
  socket.on("close", () => {
171
- console.log("Agent disconnected");
943
+ console.log("🚪 Agent disconnected");
172
944
  });
945
+ const tunnelFrame = (frame) => {
946
+ // Only allow response frames to be sent back to the agent to prevent request spoofing
947
+ if ((frame.type === buffer_1.FrameType.RESPONSE_START ||
948
+ frame.type === buffer_1.FrameType.RESPONSE_DATA ||
949
+ frame.type === buffer_1.FrameType.RESPONSE_END)) {
950
+ socket.write((0, buffer_1.encodeFrame)(frame));
951
+ }
952
+ const eventPayload = {
953
+ ...frame,
954
+ timestamp: Date.now(),
955
+ };
956
+ events_1.agentEvents.emit((0, buffer_1.getEventName)(frame.type), eventPayload);
957
+ };
173
958
  };
174
959
  exports.upgradeHandler = upgradeHandler;
960
+ const sanitizeHeaders = (headers, port) => {
961
+ const clean = {};
962
+ for (const [k, v] of Object.entries(headers || {})) {
963
+ const key = k.toLowerCase();
964
+ if (key.startsWith(":") ||
965
+ [
966
+ "connection",
967
+ "upgrade",
968
+ "content-length",
969
+ "accept-encoding",
970
+ "transfer-encoding",
971
+ ].includes(key))
972
+ continue;
973
+ clean[key] = v;
974
+ }
975
+ clean["host"] = `localhost:${port}`;
976
+ return clean;
977
+ };
175
978
 
176
979
 
177
980
  /***/ }),
@@ -182,47 +985,94 @@ exports.upgradeHandler = upgradeHandler;
182
985
  "use strict";
183
986
 
184
987
  Object.defineProperty(exports, "__esModule", ({ value: true }));
185
- exports.decodeTunnelId = exports.decodeFrames = exports.encodeFrame = exports.FrameType = void 0;
988
+ exports.decodeTunnelId = exports.decodeFrames = exports.encodeFrame = exports.getEventName = exports.EventType = exports.FrameType = void 0;
186
989
  const node_buffer_1 = __nccwpck_require__(573);
187
- const LENGTH = {
188
- LENGTH_FIELD: 4,
189
- };
190
990
  var FrameType;
191
991
  (function (FrameType) {
192
- FrameType[FrameType["REQUEST"] = 1] = "REQUEST";
193
- FrameType[FrameType["RESPONSE"] = 2] = "RESPONSE";
194
- FrameType[FrameType["REGISTERED"] = 3] = "REGISTERED";
992
+ // Tunnel initialization
993
+ FrameType[FrameType["TUNNEL_INIT"] = 0] = "TUNNEL_INIT";
994
+ // Requests
995
+ FrameType[FrameType["REQUEST_START"] = 1] = "REQUEST_START";
996
+ FrameType[FrameType["REQUEST_DATA"] = 2] = "REQUEST_DATA";
997
+ FrameType[FrameType["REQUEST_END"] = 3] = "REQUEST_END";
998
+ // Responses
999
+ FrameType[FrameType["RESPONSE_START"] = 4] = "RESPONSE_START";
1000
+ FrameType[FrameType["RESPONSE_DATA"] = 5] = "RESPONSE_DATA";
1001
+ FrameType[FrameType["RESPONSE_END"] = 6] = "RESPONSE_END";
195
1002
  })(FrameType || (exports.FrameType = FrameType = {}));
1003
+ var EventType;
1004
+ (function (EventType) {
1005
+ EventType["REQUEST_START"] = "request-start";
1006
+ EventType["REQUEST_DATA"] = "request-data";
1007
+ EventType["REQUEST_END"] = "request-end";
1008
+ EventType["RESPONSE_START"] = "response-start";
1009
+ EventType["RESPONSE_DATA"] = "response-data";
1010
+ EventType["RESPONSE_END"] = "response-end";
1011
+ EventType["UNKNOWN"] = "unknown-event";
1012
+ })(EventType || (exports.EventType = EventType = {}));
1013
+ const FrameName = {
1014
+ [FrameType.TUNNEL_INIT]: EventType.REQUEST_START, // Not really an event, but we can reuse the payload structure
1015
+ [FrameType.REQUEST_START]: EventType.REQUEST_START,
1016
+ [FrameType.REQUEST_DATA]: EventType.REQUEST_DATA,
1017
+ [FrameType.REQUEST_END]: EventType.REQUEST_END,
1018
+ [FrameType.RESPONSE_START]: EventType.RESPONSE_START,
1019
+ [FrameType.RESPONSE_DATA]: EventType.RESPONSE_DATA,
1020
+ [FrameType.RESPONSE_END]: EventType.RESPONSE_END,
1021
+ };
1022
+ const getEventName = (type) => {
1023
+ if (type in FrameName) {
1024
+ return FrameName[type];
1025
+ }
1026
+ return EventType.UNKNOWN;
1027
+ };
1028
+ exports.getEventName = getEventName;
1029
+ /**
1030
+ * Frame format:
1031
+ * [4 bytes length][1 byte type][8 bytes requestId][payload...]
1032
+ */
196
1033
  const encodeFrame = (frame) => {
197
- const data = JSON.stringify(frame);
198
- const buf = node_buffer_1.Buffer.allocUnsafe(LENGTH.LENGTH_FIELD + node_buffer_1.Buffer.byteLength(data));
199
- buf.writeInt32BE(node_buffer_1.Buffer.byteLength(data), 0);
200
- node_buffer_1.Buffer.from(data).copy(buf, LENGTH.LENGTH_FIELD);
201
- console.log(`Encoded frame ${JSON.stringify(frame)} to buffer of length ${buf.length}`);
202
- return buf;
1034
+ const payload = frame.payload instanceof node_buffer_1.Buffer
1035
+ ? frame.payload
1036
+ : frame.payload
1037
+ ? node_buffer_1.Buffer.from(JSON.stringify(frame.payload))
1038
+ : node_buffer_1.Buffer.alloc(0);
1039
+ const header = node_buffer_1.Buffer.alloc(13); // 4 + 1 + 8
1040
+ header.writeUInt32BE(payload.length + 9, 0); // total length = type(1) + requestId(8) + payload
1041
+ header.writeUInt8(frame.type, 4); // type
1042
+ header.write(frame.requestId, 5, 8, "hex"); // requestId
1043
+ return node_buffer_1.Buffer.concat([header, payload]); // final frame buffer
203
1044
  };
204
1045
  exports.encodeFrame = encodeFrame;
205
1046
  const decodeFrames = (buffer) => {
206
- let offset = 0;
207
- let len = buffer.readInt32BE(0);
208
1047
  const frames = [];
209
- while (buffer.length - offset <= LENGTH.LENGTH_FIELD + len) {
210
- const body = buffer.slice(offset + LENGTH.LENGTH_FIELD, offset + LENGTH.LENGTH_FIELD + len);
211
- frames.push(JSON.parse(body.toString()));
212
- offset += LENGTH.LENGTH_FIELD + len;
213
- if (buffer.length - offset < LENGTH.LENGTH_FIELD)
1048
+ let offset = 0;
1049
+ while (buffer.length - offset >= 4) {
1050
+ const len = buffer.readUInt32BE(offset);
1051
+ if (buffer.length - offset < len + 4)
214
1052
  break;
215
- len = buffer.readInt32BE(offset);
1053
+ const type = buffer.readUInt8(offset + 4); // type
1054
+ const requestId = buffer
1055
+ .slice(offset + 5, offset + 13)
1056
+ .toString("hex"); //
1057
+ const payloadBuf = buffer.slice(offset + 13, offset + 4 + len);
1058
+ let payload = payloadBuf;
1059
+ if (type === FrameType.TUNNEL_INIT ||
1060
+ type === FrameType.REQUEST_START ||
1061
+ type === FrameType.RESPONSE_START) {
1062
+ payload = JSON.parse(payloadBuf.toString());
1063
+ }
1064
+ frames.push({ type, requestId, payload });
1065
+ offset += len + 4;
216
1066
  }
217
1067
  return { frames, remaining: buffer.slice(offset) };
218
1068
  };
219
1069
  exports.decodeFrames = decodeFrames;
220
1070
  const decodeTunnelId = (buffer) => {
221
1071
  const data = decodeFrames(buffer).frames[0];
222
- if (data?.type !== FrameType.REGISTERED) {
1072
+ if (data?.type !== FrameType.TUNNEL_INIT) {
223
1073
  throw new Error("Invalid frame type for tunnel ID");
224
1074
  }
225
- return data.tunnelId;
1075
+ return data.payload.tunnelId.toString();
226
1076
  };
227
1077
  exports.decodeTunnelId = decodeTunnelId;
228
1078
 
@@ -4564,7 +5414,7 @@ exports.suggestSimilar = suggestSimilar;
4564
5414
  /***/ ((module) => {
4565
5415
 
4566
5416
  "use strict";
4567
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@sandeepk1729/porter","version":"1.0.1","description":"a port forwarding agent","main":"./dist/index.js","bin":{"porter":"dist/index.js"},"scripts":{"dev":"tsc -w & ncc build src/index.ts --out dist --watch","unlink":"npm unlink @sandeepk1729/jarvis -g","build":"tsc && ncc build src/index.ts --out dist","prepublishOnly":"npm run build","lint":"eslint \'src/**/*.ts\'"},"keywords":["port","agent","node","cli"],"homepage":"https://github.com/SandeepK1729/porter-agent#readme","bugs":{"url":"https://github.com/SandeepK1729/porter-agent/issues"},"repository":{"type":"git","url":"git+https://github.com/SandeepK1729/porter-agent.git"},"author":"SandeepK1729 <SandeepK1729+user@users.noreply.github.com>","license":"MIT","devDependencies":{"@types/node":"^24.3.0","@vercel/ncc":"^0.38.3","eslint":"^9.35.0","prettier":"^3.6.2","typescript":"^5.9.2"},"publishConfig":{"access":"public","directory":"dist","registry":"https://registry.npmjs.org"},"files":["dist/","README.md","package.json"],"dependencies":{"commander":"^14.0.0"}}');
5417
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@sandeepk1729/porter","version":"1.2.0","description":"a port forwarding agent","main":"./dist/index.js","bin":{"porter":"dist/index.js"},"scripts":{"dev":"tsc -w & ncc build src/index.ts --out dist --watch","unlink":"npm unlink @sandeepk1729/porter -g","build":"tsc && ncc build src/index.ts --out dist","prepublishOnly":"npm run build","lint":"eslint \'src/**/*.ts\'"},"keywords":["port","agent","node","cli"],"homepage":"https://github.com/SandeepK1729/porter-agent#readme","bugs":{"url":"https://github.com/SandeepK1729/porter-agent/issues"},"repository":{"type":"git","url":"git+https://github.com/SandeepK1729/porter-agent.git"},"author":"SandeepK1729 <SandeepK1729+user@users.noreply.github.com>","license":"MIT","devDependencies":{"@types/node":"^24.3.0","@vercel/ncc":"^0.38.3","eslint":"^9.35.0","prettier":"^3.6.2","typescript":"^5.9.2"},"publishConfig":{"access":"public","directory":"dist","registry":"https://registry.npmjs.org"},"files":["dist/","README.md","package.json"],"dependencies":{"commander":"^14.0.0"}}');
4568
5418
 
4569
5419
  /***/ })
4570
5420
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandeepk1729/porter",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "a port forwarding agent",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "dev": "tsc -w & ncc build src/index.ts --out dist --watch",
11
- "unlink": "npm unlink @sandeepk1729/jarvis -g",
11
+ "unlink": "npm unlink @sandeepk1729/porter -g",
12
12
  "build": "tsc && ncc build src/index.ts --out dist",
13
13
  "prepublishOnly": "npm run build",
14
14
  "lint": "eslint 'src/**/*.ts'"