portprism 2.0.1 → 2.0.3

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.
@@ -0,0 +1,58 @@
1
+ /** Raw event the Agent emits on its `traffic` channel. */
2
+ export interface TrafficEvent {
3
+ phase: "request" | "response";
4
+ id: string;
5
+ ts?: number;
6
+ method?: string;
7
+ path?: string;
8
+ /** base64 (as it travels on the wire); decoded for display. */
9
+ body?: string;
10
+ headers?: Record<string, string>;
11
+ status?: number;
12
+ durationMs?: number;
13
+ }
14
+ /** A decoded request/response body plus how to interpret it. */
15
+ interface DecodedBody {
16
+ /** Decoded UTF-8 text, or base64 for binary payloads. `null` when empty. */
17
+ text: string | null;
18
+ binary: boolean;
19
+ /** Byte length of the full (pre-truncation) payload. */
20
+ size: number;
21
+ truncated: boolean;
22
+ }
23
+ /** One fully-merged entry in the ledger. */
24
+ export interface RequestRecord {
25
+ id: string;
26
+ ts: number;
27
+ method: string;
28
+ path: string;
29
+ query: Record<string, string> | null;
30
+ reqHeaders: Record<string, string>;
31
+ reqBody: DecodedBody;
32
+ status?: number;
33
+ resHeaders?: Record<string, string>;
34
+ resBody?: DecodedBody;
35
+ durationMs?: number;
36
+ pending: boolean;
37
+ }
38
+ export declare class Inspector {
39
+ private readonly records;
40
+ private readonly byId;
41
+ private readonly sseClients;
42
+ private server;
43
+ private boundPort;
44
+ /** Ingest one traffic event from the agent, merging by request id. */
45
+ record(evt: TrafficEvent): void;
46
+ /** Compact summary used by the list view. */
47
+ private summary;
48
+ private broadcast;
49
+ /**
50
+ * Start the inspector HTTP server. Resolves with the bound URL, or `null` if the
51
+ * port is already taken (the tunnel must never fail because :4040 is busy).
52
+ */
53
+ start(port: number): Promise<string | null>;
54
+ stop(): void;
55
+ private handle;
56
+ }
57
+ export {};
58
+ //# sourceMappingURL=inspector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspector.d.ts","sourceRoot":"","sources":["../src/inspector.ts"],"names":[],"mappings":"AAgBA,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,gEAAgE;AAChE,UAAU,WAAW;IACnB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB;AA6CD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoC;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,SAAS,CAAuB;IAExC,sEAAsE;IACtE,MAAM,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAmC/B,6CAA6C;IAC7C,OAAO,CAAC,OAAO;IAef,OAAO,CAAC,SAAS;IAYjB;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAkB3C,IAAI,IAAI,IAAI;IAaZ,OAAO,CAAC,MAAM;CA8Cf"}
@@ -0,0 +1,328 @@
1
+ /**
2
+ * inspector.ts — the `--inspect` Local Request Inspector.
3
+ *
4
+ * An ngrok-style traffic ledger served on localhost:4040. Every HTTP request the
5
+ * agent forwards to the local server is recorded here with its headers, query
6
+ * params, and (decoded) bodies, then surfaced through a small web UI and a JSON
7
+ * API — handy for webhook / API debugging.
8
+ *
9
+ * The agent emits `traffic` events ({ phase: "request" | "response", ... }); this
10
+ * module records them, merges the two phases by id, and pushes live updates to any
11
+ * connected browser over Server-Sent Events. Built on Node's native `http` — no
12
+ * extra dependency ships in the published CLI.
13
+ */
14
+ import http from "node:http";
15
+ const MAX_RECORDS = 200;
16
+ const MAX_BODY_BYTES = 256 * 1024;
17
+ /** Content types we render as text rather than base64. */
18
+ const TEXT_CONTENT_RE = /(json|text\/|xml|x-www-form-urlencoded|javascript|html|csv|graphql)/i;
19
+ /** Split a raw request path into its pathname and parsed query object. */
20
+ function splitPath(rawPath) {
21
+ const qIdx = rawPath.indexOf("?");
22
+ if (qIdx === -1)
23
+ return { path: rawPath, query: null };
24
+ const path = rawPath.slice(0, qIdx);
25
+ const query = {};
26
+ const params = new URLSearchParams(rawPath.slice(qIdx + 1));
27
+ for (const [k, v] of params)
28
+ query[k] = v;
29
+ return { path, query: Object.keys(query).length ? query : null };
30
+ }
31
+ /** Decode a base64 wire body into text (or capped base64 for binary). */
32
+ function decodeBody(base64, headers) {
33
+ if (!base64)
34
+ return { text: null, binary: false, size: 0, truncated: false };
35
+ const buf = Buffer.from(base64, "base64");
36
+ const size = buf.length;
37
+ const truncated = size > MAX_BODY_BYTES;
38
+ const slice = truncated ? buf.subarray(0, MAX_BODY_BYTES) : buf;
39
+ const contentType = (headers?.["content-type"] ?? "").toLowerCase();
40
+ const looksText = contentType
41
+ ? TEXT_CONTENT_RE.test(contentType)
42
+ : !slice.includes(0); // no content-type: treat as text unless it has NUL bytes
43
+ if (looksText) {
44
+ return { text: slice.toString("utf8"), binary: false, size, truncated };
45
+ }
46
+ return { text: slice.toString("base64"), binary: true, size, truncated };
47
+ }
48
+ export class Inspector {
49
+ records = [];
50
+ byId = new Map();
51
+ sseClients = new Set();
52
+ server = null;
53
+ boundPort = null;
54
+ /** Ingest one traffic event from the agent, merging by request id. */
55
+ record(evt) {
56
+ if (evt.phase === "request") {
57
+ const { path, query } = splitPath(evt.path ?? "");
58
+ const rec = {
59
+ id: evt.id,
60
+ ts: evt.ts ?? Date.now(),
61
+ method: evt.method ?? "GET",
62
+ path,
63
+ query,
64
+ reqHeaders: evt.headers ?? {},
65
+ reqBody: decodeBody(evt.body, evt.headers),
66
+ pending: true,
67
+ };
68
+ this.byId.set(rec.id, rec);
69
+ this.records.push(rec);
70
+ // Evict oldest beyond the cap.
71
+ while (this.records.length > MAX_RECORDS) {
72
+ const dropped = this.records.shift();
73
+ if (dropped)
74
+ this.byId.delete(dropped.id);
75
+ }
76
+ this.broadcast(rec);
77
+ return;
78
+ }
79
+ // response phase — merge into the existing record.
80
+ const rec = this.byId.get(evt.id);
81
+ if (!rec)
82
+ return;
83
+ rec.status = evt.status;
84
+ rec.resHeaders = evt.headers ?? {};
85
+ rec.resBody = decodeBody(evt.body, evt.headers);
86
+ rec.durationMs = evt.durationMs;
87
+ rec.pending = false;
88
+ this.broadcast(rec);
89
+ }
90
+ /** Compact summary used by the list view. */
91
+ summary(r) {
92
+ return {
93
+ id: r.id,
94
+ ts: r.ts,
95
+ method: r.method,
96
+ path: r.path,
97
+ status: r.status ?? null,
98
+ durationMs: r.durationMs ?? null,
99
+ pending: r.pending,
100
+ reqSize: r.reqBody.size,
101
+ resSize: r.resBody?.size ?? 0,
102
+ resContentType: r.resHeaders?.["content-type"] ?? null,
103
+ };
104
+ }
105
+ broadcast(rec) {
106
+ if (this.sseClients.size === 0)
107
+ return;
108
+ const line = `data: ${JSON.stringify(this.summary(rec))}\n\n`;
109
+ for (const res of this.sseClients) {
110
+ try {
111
+ res.write(line);
112
+ }
113
+ catch {
114
+ /* client gone; cleaned up on close */
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * Start the inspector HTTP server. Resolves with the bound URL, or `null` if the
120
+ * port is already taken (the tunnel must never fail because :4040 is busy).
121
+ */
122
+ start(port) {
123
+ return new Promise((resolve) => {
124
+ const server = http.createServer((req, res) => this.handle(req, res));
125
+ server.once("error", (err) => {
126
+ if (err.code === "EADDRINUSE") {
127
+ resolve(null);
128
+ }
129
+ else {
130
+ resolve(null);
131
+ }
132
+ });
133
+ server.listen(port, "127.0.0.1", () => {
134
+ this.server = server;
135
+ this.boundPort = server.address().port;
136
+ resolve(`http://localhost:${this.boundPort}`);
137
+ });
138
+ });
139
+ }
140
+ stop() {
141
+ for (const res of this.sseClients) {
142
+ try {
143
+ res.end();
144
+ }
145
+ catch {
146
+ /* ignore */
147
+ }
148
+ }
149
+ this.sseClients.clear();
150
+ this.server?.close();
151
+ this.server = null;
152
+ }
153
+ handle(req, res) {
154
+ const url = req.url ?? "/";
155
+ if (url === "/" || url === "/index.html") {
156
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
157
+ res.end(INDEX_HTML);
158
+ return;
159
+ }
160
+ if (url === "/__pp/requests") {
161
+ const body = JSON.stringify(this.records.map((r) => this.summary(r)).reverse());
162
+ res.writeHead(200, { "content-type": "application/json" });
163
+ res.end(body);
164
+ return;
165
+ }
166
+ if (url.startsWith("/__pp/requests/")) {
167
+ const id = decodeURIComponent(url.slice("/__pp/requests/".length));
168
+ const rec = this.byId.get(id);
169
+ if (!rec) {
170
+ res.writeHead(404, { "content-type": "application/json" });
171
+ res.end(JSON.stringify({ error: "not found" }));
172
+ return;
173
+ }
174
+ res.writeHead(200, { "content-type": "application/json" });
175
+ res.end(JSON.stringify(rec));
176
+ return;
177
+ }
178
+ if (url === "/__pp/stream") {
179
+ res.writeHead(200, {
180
+ "content-type": "text/event-stream",
181
+ "cache-control": "no-cache",
182
+ connection: "keep-alive",
183
+ });
184
+ res.write("retry: 3000\n\n");
185
+ this.sseClients.add(res);
186
+ req.on("close", () => this.sseClients.delete(res));
187
+ return;
188
+ }
189
+ res.writeHead(404, { "content-type": "text/plain" });
190
+ res.end("Not found");
191
+ }
192
+ }
193
+ // ── Web UI ──────────────────────────────────────────────────────────────────────
194
+ // Single self-contained page. Client JS uses string concatenation (no template
195
+ // literals) so it survives being embedded in this module's own template literal.
196
+ const INDEX_HTML = `<!doctype html>
197
+ <html lang="en">
198
+ <head>
199
+ <meta charset="utf-8">
200
+ <meta name="viewport" content="width=device-width, initial-scale=1">
201
+ <title>PortPrism Inspector</title>
202
+ <style>
203
+ :root { color-scheme: dark; }
204
+ * { box-sizing: border-box; }
205
+ body { margin:0; font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
206
+ background:#0b0d12; color:#e6e8ee; }
207
+ header { display:flex; align-items:center; gap:10px; padding:12px 16px;
208
+ border-bottom:1px solid #1c2030; background:#0e1017; position:sticky; top:0; }
209
+ header .logo { font-weight:700; background:linear-gradient(90deg,#6366f1,#8b5cf6,#34d399);
210
+ -webkit-background-clip:text; background-clip:text; color:transparent; }
211
+ header .dim { color:#7b8194; font-size:12px; }
212
+ header .live { margin-left:auto; display:flex; align-items:center; gap:6px; color:#7b8194; }
213
+ header .dot { width:8px; height:8px; border-radius:50%; background:#34d399; box-shadow:0 0 8px #34d399; }
214
+ .layout { display:grid; grid-template-columns: 1fr; }
215
+ table { width:100%; border-collapse:collapse; }
216
+ th,td { text-align:left; padding:7px 12px; border-bottom:1px solid #151824; white-space:nowrap; }
217
+ th { color:#7b8194; font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.05em; }
218
+ tr.row { cursor:pointer; }
219
+ tr.row:hover { background:#12151f; }
220
+ .method { font-weight:700; }
221
+ .m-GET{color:#34d399} .m-POST{color:#60a5fa} .m-PUT{color:#fbbf24}
222
+ .m-DELETE{color:#f87171} .m-PATCH{color:#c084fc}
223
+ .st-2{color:#34d399} .st-3{color:#60a5fa} .st-4{color:#fbbf24} .st-5{color:#f87171}
224
+ .pending{color:#7b8194}
225
+ .path { color:#e6e8ee; max-width:48vw; overflow:hidden; text-overflow:ellipsis; }
226
+ .empty { padding:48px 16px; text-align:center; color:#7b8194; }
227
+ dialog { width:min(860px,92vw); max-height:86vh; overflow:auto; background:#0e1017;
228
+ color:#e6e8ee; border:1px solid #262b3d; border-radius:12px; padding:0; }
229
+ dialog::backdrop { background:rgba(0,0,0,.6); }
230
+ .d-head { display:flex; align-items:center; gap:10px; padding:14px 18px; border-bottom:1px solid #1c2030;
231
+ position:sticky; top:0; background:#0e1017; }
232
+ .d-head .x { margin-left:auto; cursor:pointer; color:#7b8194; background:none; border:none; font-size:18px; }
233
+ .d-body { padding:14px 18px; }
234
+ h4 { margin:18px 0 6px; color:#7b8194; font-size:11px; text-transform:uppercase; letter-spacing:.05em; }
235
+ h4:first-child { margin-top:0; }
236
+ pre { margin:0; padding:10px 12px; background:#0b0d12; border:1px solid #151824; border-radius:8px;
237
+ overflow:auto; white-space:pre-wrap; word-break:break-word; }
238
+ .kv { display:grid; grid-template-columns:200px 1fr; gap:2px 12px; }
239
+ .kv .k { color:#7b8194; } .kv .v { word-break:break-word; }
240
+ .tag { font-size:11px; color:#7b8194; }
241
+ </style>
242
+ </head>
243
+ <body>
244
+ <header>
245
+ <span class="logo">PortPrism</span><span class="dim">Request Inspector</span>
246
+ <span class="live"><span class="dot"></span> live</span>
247
+ </header>
248
+ <div class="layout">
249
+ <table>
250
+ <thead><tr><th>Time</th><th>Method</th><th>Status</th><th>Path</th><th>Dur</th><th>Size</th></tr></thead>
251
+ <tbody id="rows"></tbody>
252
+ </table>
253
+ <div class="empty" id="empty">Waiting for requests… send traffic through your tunnel.</div>
254
+ </div>
255
+ <dialog id="dlg"><div class="d-head"><b id="d-title"></b><button class="x" id="d-close">✕</button></div><div class="d-body" id="d-body"></div></dialog>
256
+ <script>
257
+ (function(){
258
+ var rows = document.getElementById("rows");
259
+ var empty = document.getElementById("empty");
260
+ var dlg = document.getElementById("dlg");
261
+ var seen = {};
262
+
263
+ function esc(s){ return String(s).replace(/[&<>"]/g, function(c){
264
+ return {"&":"&amp;","<":"&lt;",">":"&gt;","\\"":"&quot;"}[c]; }); }
265
+ function mclass(m){ return "method m-" + esc(m); }
266
+ function sclass(s){ return s ? "st-" + String(s)[0] : "pending"; }
267
+ function fmtTime(ts){ var d = new Date(ts); return d.toLocaleTimeString(); }
268
+ function fmtSize(n){ if(!n) return "—"; if(n<1024) return n+" B"; return (n/1024).toFixed(1)+" KB"; }
269
+ function fmtDur(d){ return d==null ? "…" : d+" ms"; }
270
+
271
+ function upsert(s){
272
+ empty.style.display = "none";
273
+ var id = s.id;
274
+ var tr = document.getElementById("r-"+id);
275
+ var html = "<td>"+fmtTime(s.ts)+"</td>"
276
+ + "<td class='"+mclass(s.method)+"'>"+esc(s.method)+"</td>"
277
+ + "<td class='"+sclass(s.status)+"'>"+(s.status||"…")+"</td>"
278
+ + "<td class='path'>"+esc(s.path)+"</td>"
279
+ + "<td class='tag'>"+fmtDur(s.durationMs)+"</td>"
280
+ + "<td class='tag'>"+fmtSize(s.resSize||s.reqSize)+"</td>";
281
+ if(tr){ tr.innerHTML = html; }
282
+ else {
283
+ tr = document.createElement("tr");
284
+ tr.className = "row"; tr.id = "r-"+id; tr.innerHTML = html;
285
+ tr.onclick = function(){ openDetail(id); };
286
+ rows.insertBefore(tr, rows.firstChild);
287
+ }
288
+ }
289
+
290
+ function section(title, obj){
291
+ if(!obj || !Object.keys(obj).length) return "";
292
+ var out = "<h4>"+esc(title)+"</h4><div class='kv'>";
293
+ Object.keys(obj).forEach(function(k){
294
+ out += "<div class='k'>"+esc(k)+"</div><div class='v'>"+esc(obj[k])+"</div>";
295
+ });
296
+ return out + "</div>";
297
+ }
298
+ function bodySection(title, b){
299
+ if(!b || !b.text) return "";
300
+ var note = (b.binary ? " (binary, base64)" : "") + (b.truncated ? " (truncated)" : "");
301
+ return "<h4>"+esc(title)+note+"</h4><pre>"+esc(b.text)+"</pre>";
302
+ }
303
+
304
+ function openDetail(id){
305
+ fetch("/__pp/requests/"+encodeURIComponent(id)).then(function(r){return r.json();}).then(function(d){
306
+ document.getElementById("d-title").textContent = d.method+" "+d.path
307
+ + (d.status ? " → "+d.status : "");
308
+ var html = section("Query", d.query)
309
+ + section("Request headers", d.reqHeaders)
310
+ + bodySection("Request body", d.reqBody)
311
+ + section("Response headers", d.resHeaders)
312
+ + bodySection("Response body", d.resBody);
313
+ document.getElementById("d-body").innerHTML = html || "<p class='tag'>No details.</p>";
314
+ dlg.showModal();
315
+ });
316
+ }
317
+ document.getElementById("d-close").onclick = function(){ dlg.close(); };
318
+
319
+ fetch("/__pp/requests").then(function(r){return r.json();}).then(function(list){
320
+ list.reverse().forEach(upsert);
321
+ });
322
+ var es = new EventSource("/__pp/stream");
323
+ es.onmessage = function(e){ try { upsert(JSON.parse(e.data)); } catch(_){} };
324
+ })();
325
+ </script>
326
+ </body>
327
+ </html>`;
328
+ //# sourceMappingURL=inspector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspector.js","sourceRoot":"","sources":["../src/inspector.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "portprism",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
+ "license": "MIT",
4
5
  "description": "Share your local server with anyone, instantly.",
5
6
  "type": "module",
6
7
  "bin": {
@@ -16,12 +17,13 @@
16
17
  "scripts": {
17
18
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\nimport { createRequire as _createRequire } from 'module';\nimport { fileURLToPath as _fileURLToPath } from 'url';\nconst require = _createRequire(import.meta.url);\nconst __filename = _fileURLToPath(import.meta.url);\nconst __dirname = _fileURLToPath(new URL('.', import.meta.url));\"",
18
19
  "dev": "tsc --watch",
19
- "prepublishOnly": "npm run build"
20
+ "lint": "eslint src/",
21
+ "prepublishOnly": "npm run build",
22
+ "test": "echo \"No tests yet\" && exit 0"
20
23
  },
21
24
  "dependencies": {
22
25
  "chalk": "^4.1.2",
23
26
  "commander": "^8.3.0",
24
- "dotenv": "^10.0.0",
25
27
  "open": "^8.4.2",
26
28
  "ora": "^9.4.0",
27
29
  "puppeteer-core": "^25.0.2",
package/README.md DELETED
@@ -1,12 +0,0 @@
1
- # PortPrism
2
-
3
- **Share your local dev server with anyone, instantly.** No account, no config —
4
- point PortLens at a local port and you get a clean, branded preview URL that
5
- recipients open in any browser.
6
-
7
- - 🔗 **Instant public URL** for any local HTTP server
8
- - 🖥️ **Branded viewer** — recipients see your app, not a tunnel warning page
9
- - 🔒 **TLS end-to-end**; optional password protection (Pro)
10
- - 🔁 **Auto-reconnect** — brief network drops recover on the same URL
11
- - 📱 **QR code** for quick mobile testing
12
- - 🤖 **`--json` output** for scripts and CI