@open-probe/proxy 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +355 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +281 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 open-probe contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @open-probe/proxy
|
|
2
|
+
|
|
3
|
+
Reverse proxy that injects [`@open-probe/core`](../core) into any HTML
|
|
4
|
+
response, so you can annotate elements on **any** dev server (Vite, Next,
|
|
5
|
+
Nuxt, SvelteKit, plain Express…) without touching its build config.
|
|
6
|
+
|
|
7
|
+
Also bundles a minimal hand-rolled WebSocket annotation sink for the
|
|
8
|
+
`server (ws)` transport, so you can pipe annotations to a file, stdout,
|
|
9
|
+
or a custom editor integration without an extra service.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add -D @open-probe/proxy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## CLI
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Inject open-probe into any dev server
|
|
21
|
+
npx open-probe-proxy --target http://localhost:5173 --port 4000
|
|
22
|
+
|
|
23
|
+
# Plus a WebSocket sink that appends each annotation to a file
|
|
24
|
+
npx open-probe-proxy \
|
|
25
|
+
--target http://localhost:5173 \
|
|
26
|
+
--port 4000 \
|
|
27
|
+
--ws --ws-output annotations.jsonl
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Flag | Default | Description |
|
|
31
|
+
|------|---------|-------------|
|
|
32
|
+
| `--target <url>` | (required) | Upstream dev server to proxy. |
|
|
33
|
+
| `--port <n>` | `4000` | Port the proxy listens on. |
|
|
34
|
+
| `--host <h>` | `127.0.0.1` | Bind address. |
|
|
35
|
+
| `--transport <t>` | `clipboard` | `clipboard` / `mcp` / `server`. |
|
|
36
|
+
| `--mcp-endpoint <url>` | `http://127.0.0.1:3100` | MCP HTTP endpoint when `--transport=mcp`. |
|
|
37
|
+
| `--ws` | off | Attach a WebSocket annotation sink at `--ws-path`. |
|
|
38
|
+
| `--ws-path <p>` | `/__open-probe/ws` | WebSocket upgrade path. |
|
|
39
|
+
| `--ws-output <f>` | stdout | File to append received annotation JSON. |
|
|
40
|
+
|
|
41
|
+
## Programmatic API
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { createServer, attachWsSink } from '@open-probe/proxy';
|
|
45
|
+
|
|
46
|
+
const server = createServer({
|
|
47
|
+
target: 'http://localhost:5173',
|
|
48
|
+
port: 4000,
|
|
49
|
+
injectScript: { transport: 'mcp', mcpEndpoint: 'http://127.0.0.1:3100' },
|
|
50
|
+
});
|
|
51
|
+
attachWsSink(server, { path: '/__open-probe/ws', onMessage: ann => save(ann) });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import cac from "cac";
|
|
5
|
+
import { appendFileSync } from "fs";
|
|
6
|
+
import { resolve as resolve2 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/server.ts
|
|
9
|
+
import { createServer } from "http";
|
|
10
|
+
import { request as httpRequest } from "http";
|
|
11
|
+
import { request as httpsRequest } from "https";
|
|
12
|
+
import { URL } from "url";
|
|
13
|
+
import { promises as fs } from "fs";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import { dirname, resolve } from "path";
|
|
16
|
+
|
|
17
|
+
// src/injector.ts
|
|
18
|
+
var HEAD_MATCH = /<head([^>]*)>/i;
|
|
19
|
+
function buildInjection(scriptUrl, config) {
|
|
20
|
+
const json = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
21
|
+
return `<script>window.__OPEN_PROBE_CONFIG__=${json};</script><script src="${scriptUrl}" defer></script>`;
|
|
22
|
+
}
|
|
23
|
+
function injectIntoHtml(html, snippet) {
|
|
24
|
+
if (!HEAD_MATCH.test(html)) {
|
|
25
|
+
return html.replace(/<html([^>]*)>/i, `<html$1><head>${snippet}</head>`);
|
|
26
|
+
}
|
|
27
|
+
return html.replace(HEAD_MATCH, (match) => `${match}${snippet}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/ws-sink.ts
|
|
31
|
+
import { createHash } from "crypto";
|
|
32
|
+
var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
33
|
+
function attachWsSink(server, opts) {
|
|
34
|
+
server.on("upgrade", (req, socket) => {
|
|
35
|
+
if ((req.url ?? "/") !== opts.path) {
|
|
36
|
+
socket.destroy();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const key = req.headers["sec-websocket-key"];
|
|
40
|
+
if (typeof key !== "string" || !key) {
|
|
41
|
+
socket.destroy();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const accept = createHash("sha1").update(`${key}${GUID}`).digest("base64");
|
|
45
|
+
socket.write(
|
|
46
|
+
`HTTP/1.1 101 Switching Protocols\r
|
|
47
|
+
Upgrade: websocket\r
|
|
48
|
+
Connection: Upgrade\r
|
|
49
|
+
Sec-WebSocket-Accept: ${accept}\r
|
|
50
|
+
\r
|
|
51
|
+
`
|
|
52
|
+
);
|
|
53
|
+
const remote = req.socket.remoteAddress ?? "";
|
|
54
|
+
pumpFrames(
|
|
55
|
+
socket,
|
|
56
|
+
(text) => opts.onMessage(text, { remoteAddress: remote }),
|
|
57
|
+
(err) => opts.onError?.(err)
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function pumpFrames(socket, onText, onError) {
|
|
62
|
+
let buffer = Buffer.alloc(0);
|
|
63
|
+
let textChunks = [];
|
|
64
|
+
socket.on("data", (chunk) => {
|
|
65
|
+
buffer = buffer.length ? Buffer.concat([buffer, chunk]) : chunk;
|
|
66
|
+
while (buffer.length >= 2) {
|
|
67
|
+
const frame = parseFrame(buffer);
|
|
68
|
+
if (!frame) return;
|
|
69
|
+
buffer = buffer.subarray(frame.consumed);
|
|
70
|
+
if (frame.opcode === 0 || frame.opcode === 1) {
|
|
71
|
+
textChunks.push(frame.payload);
|
|
72
|
+
if (frame.fin) {
|
|
73
|
+
const full = Buffer.concat(textChunks).toString("utf8");
|
|
74
|
+
textChunks = [];
|
|
75
|
+
try {
|
|
76
|
+
onText(full);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
onError(err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} else if (frame.opcode === 8) {
|
|
82
|
+
socket.end(encodeFrame(8, frame.payload));
|
|
83
|
+
return;
|
|
84
|
+
} else if (frame.opcode === 9) {
|
|
85
|
+
try {
|
|
86
|
+
socket.write(encodeFrame(10, frame.payload));
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
socket.on("error", (err) => onError(err));
|
|
93
|
+
}
|
|
94
|
+
function parseFrame(buffer) {
|
|
95
|
+
if (buffer.length < 2) return null;
|
|
96
|
+
const b0 = buffer[0];
|
|
97
|
+
const b1 = buffer[1];
|
|
98
|
+
const fin = (b0 & 128) !== 0;
|
|
99
|
+
const opcode = b0 & 15;
|
|
100
|
+
const masked = (b1 & 128) !== 0;
|
|
101
|
+
let payloadLen = b1 & 127;
|
|
102
|
+
let offset = 2;
|
|
103
|
+
if (payloadLen === 126) {
|
|
104
|
+
if (buffer.length < offset + 2) return null;
|
|
105
|
+
payloadLen = buffer.readUInt16BE(offset);
|
|
106
|
+
offset += 2;
|
|
107
|
+
} else if (payloadLen === 127) {
|
|
108
|
+
if (buffer.length < offset + 8) return null;
|
|
109
|
+
const hi = buffer.readUInt32BE(offset);
|
|
110
|
+
const lo = buffer.readUInt32BE(offset + 4);
|
|
111
|
+
if (hi !== 0 || lo > 2147483647) return null;
|
|
112
|
+
payloadLen = lo;
|
|
113
|
+
offset += 8;
|
|
114
|
+
}
|
|
115
|
+
let mask = null;
|
|
116
|
+
if (masked) {
|
|
117
|
+
if (buffer.length < offset + 4) return null;
|
|
118
|
+
mask = buffer.subarray(offset, offset + 4);
|
|
119
|
+
offset += 4;
|
|
120
|
+
}
|
|
121
|
+
if (buffer.length < offset + payloadLen) return null;
|
|
122
|
+
let payload = buffer.subarray(offset, offset + payloadLen);
|
|
123
|
+
if (mask) {
|
|
124
|
+
const out = Buffer.allocUnsafe(payloadLen);
|
|
125
|
+
for (let i = 0; i < payloadLen; i++) out[i] = payload[i] ^ mask[i & 3];
|
|
126
|
+
payload = out;
|
|
127
|
+
}
|
|
128
|
+
return { fin, opcode, payload, consumed: offset + payloadLen };
|
|
129
|
+
}
|
|
130
|
+
function encodeFrame(opcode, payload) {
|
|
131
|
+
const len = payload.length;
|
|
132
|
+
let header;
|
|
133
|
+
if (len < 126) {
|
|
134
|
+
header = Buffer.from([128 | opcode, len]);
|
|
135
|
+
} else if (len < 65536) {
|
|
136
|
+
header = Buffer.alloc(4);
|
|
137
|
+
header[0] = 128 | opcode;
|
|
138
|
+
header[1] = 126;
|
|
139
|
+
header.writeUInt16BE(len, 2);
|
|
140
|
+
} else {
|
|
141
|
+
header = Buffer.alloc(10);
|
|
142
|
+
header[0] = 128 | opcode;
|
|
143
|
+
header[1] = 127;
|
|
144
|
+
header.writeUInt32BE(0, 2);
|
|
145
|
+
header.writeUInt32BE(len, 6);
|
|
146
|
+
}
|
|
147
|
+
return Buffer.concat([header, payload]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/server.ts
|
|
151
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
152
|
+
var __dirname = dirname(__filename);
|
|
153
|
+
var DEFAULT_RUNTIME_PATH = "/__open-probe/runtime.js";
|
|
154
|
+
async function locateScriptFile(override) {
|
|
155
|
+
if (override) return override;
|
|
156
|
+
const candidates = [
|
|
157
|
+
resolve(__dirname, "../../core/dist/index.global.js"),
|
|
158
|
+
resolve(__dirname, "../../../core/dist/index.global.js"),
|
|
159
|
+
resolve(process.cwd(), "node_modules/@open-probe/core/dist/index.global.js")
|
|
160
|
+
];
|
|
161
|
+
for (const path of candidates) {
|
|
162
|
+
try {
|
|
163
|
+
await fs.access(path);
|
|
164
|
+
return path;
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
throw new Error(
|
|
169
|
+
"Could not locate @open-probe/core IIFE bundle. Run `pnpm --filter @open-probe/core build` first."
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function isHtmlAccept(req) {
|
|
173
|
+
const accept = req.headers.accept;
|
|
174
|
+
if (!accept) return false;
|
|
175
|
+
return accept.includes("text/html");
|
|
176
|
+
}
|
|
177
|
+
async function createProxyServer(options) {
|
|
178
|
+
const targetUrl = new URL(options.target);
|
|
179
|
+
const runtimePath = options.runtimePath ?? DEFAULT_RUNTIME_PATH;
|
|
180
|
+
const scriptFile = await locateScriptFile(options.scriptFile);
|
|
181
|
+
const injection = buildInjection(runtimePath, options.config ?? {});
|
|
182
|
+
const requestFn = targetUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
|
183
|
+
const server = createServer((req, res) => {
|
|
184
|
+
handle(req, res).catch((err) => {
|
|
185
|
+
console.error("[open-probe proxy] error:", err);
|
|
186
|
+
if (!res.headersSent) {
|
|
187
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
188
|
+
}
|
|
189
|
+
res.end(`open-probe proxy error: ${err.message}`);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
if (options.wsSink) {
|
|
193
|
+
attachWsSink(server, options.wsSink);
|
|
194
|
+
}
|
|
195
|
+
async function handle(req, res) {
|
|
196
|
+
const url = req.url ?? "/";
|
|
197
|
+
if (url === runtimePath) {
|
|
198
|
+
try {
|
|
199
|
+
const code = await fs.readFile(scriptFile, "utf-8");
|
|
200
|
+
res.writeHead(200, {
|
|
201
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
202
|
+
"Cache-Control": "no-cache",
|
|
203
|
+
"Access-Control-Allow-Origin": "*"
|
|
204
|
+
});
|
|
205
|
+
res.end(code);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
res.writeHead(500);
|
|
208
|
+
res.end(`Failed to read runtime: ${err.message}`);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (url === "/__open-probe/health") {
|
|
213
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
214
|
+
res.end(JSON.stringify({ ok: true, target: options.target }));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
proxyRequest(req, res);
|
|
218
|
+
}
|
|
219
|
+
function proxyRequest(req, res) {
|
|
220
|
+
const headers = { ...req.headers };
|
|
221
|
+
headers.host = targetUrl.host;
|
|
222
|
+
delete headers["accept-encoding"];
|
|
223
|
+
const upstream = requestFn(
|
|
224
|
+
{
|
|
225
|
+
protocol: targetUrl.protocol,
|
|
226
|
+
host: targetUrl.hostname,
|
|
227
|
+
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
228
|
+
method: req.method,
|
|
229
|
+
path: req.url,
|
|
230
|
+
headers
|
|
231
|
+
},
|
|
232
|
+
(proxyRes) => {
|
|
233
|
+
const upstreamHeaders = { ...proxyRes.headers };
|
|
234
|
+
delete upstreamHeaders["content-length"];
|
|
235
|
+
delete upstreamHeaders["content-encoding"];
|
|
236
|
+
const ct = String(proxyRes.headers["content-type"] ?? "");
|
|
237
|
+
const wantsInjection = ct.includes("text/html") && isHtmlAccept(req);
|
|
238
|
+
if (!wantsInjection) {
|
|
239
|
+
res.writeHead(proxyRes.statusCode ?? 502, upstreamHeaders);
|
|
240
|
+
proxyRes.pipe(res);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const chunks = [];
|
|
244
|
+
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
245
|
+
proxyRes.on("end", () => {
|
|
246
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
247
|
+
const injected = injectIntoHtml(body, injection);
|
|
248
|
+
res.writeHead(proxyRes.statusCode ?? 200, upstreamHeaders);
|
|
249
|
+
res.end(injected);
|
|
250
|
+
});
|
|
251
|
+
proxyRes.on("error", (err) => {
|
|
252
|
+
if (!res.headersSent) res.writeHead(502);
|
|
253
|
+
res.end(`upstream error: ${err.message}`);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
upstream.on("error", (err) => {
|
|
258
|
+
if (!res.headersSent) res.writeHead(502, { "Content-Type": "text/plain" });
|
|
259
|
+
res.end(`upstream connection failed: ${err.message}`);
|
|
260
|
+
});
|
|
261
|
+
req.pipe(upstream);
|
|
262
|
+
}
|
|
263
|
+
return new Promise((resolveStart, rejectStart) => {
|
|
264
|
+
server.once("error", rejectStart);
|
|
265
|
+
server.listen(options.port, options.host, () => {
|
|
266
|
+
const address = server.address();
|
|
267
|
+
if (typeof address === "object" && address) {
|
|
268
|
+
resolveStart({
|
|
269
|
+
address: { host: options.host, port: address.port },
|
|
270
|
+
close: () => new Promise((done) => {
|
|
271
|
+
server.close(() => done());
|
|
272
|
+
})
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
rejectStart(new Error("failed to bind proxy server"));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/cli.ts
|
|
282
|
+
var VERSION = "0.1.0";
|
|
283
|
+
var cli = cac("open-probe");
|
|
284
|
+
cli.command("[]", "Start the open-probe reverse proxy").option("-t, --target <url>", "Upstream dev server URL (e.g. http://localhost:3000)").option("-p, --port <port>", "Proxy port", { default: "4000" }).option("--host <host>", "Bind address", { default: "127.0.0.1" }).option("--transport <name>", "clipboard | mcp | server", { default: "clipboard" }).option("--output-level <level>", "compact | standard | detailed | forensic", {
|
|
285
|
+
default: "standard"
|
|
286
|
+
}).option("--mcp-endpoint <url>", "MCP HTTP base URL", {
|
|
287
|
+
default: "http://127.0.0.1:3100"
|
|
288
|
+
}).option("--server-endpoint <url>", "Editor server endpoint (for transport=server)").option("--ws", "Enable WebSocket annotation sink on the proxy port").option("--ws-path <path>", "WebSocket upgrade path", { default: "/__open-probe/ws" }).option("--ws-output <file>", "Append received annotation payloads to FILE (default: stdout)").option("--debug", "Enable verbose logging").action(async (opts) => {
|
|
289
|
+
const target = opts.target;
|
|
290
|
+
if (!target) {
|
|
291
|
+
console.error("error: --target is required");
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
const port = Number(opts.port);
|
|
295
|
+
const host = String(opts.host);
|
|
296
|
+
const config = {
|
|
297
|
+
transport: String(opts.transport),
|
|
298
|
+
outputLevel: String(opts.outputLevel),
|
|
299
|
+
mcp: { endpoint: String(opts.mcpEndpoint) },
|
|
300
|
+
debug: Boolean(opts.debug)
|
|
301
|
+
};
|
|
302
|
+
if (opts.serverEndpoint) {
|
|
303
|
+
config.server = { endpoint: String(opts.serverEndpoint), protocol: "http" };
|
|
304
|
+
}
|
|
305
|
+
let wsSink;
|
|
306
|
+
const wsPath = String(opts.wsPath ?? "/__open-probe/ws");
|
|
307
|
+
if (opts.ws) {
|
|
308
|
+
const wsOutputPath = opts.wsOutput ? resolve2(String(opts.wsOutput)) : null;
|
|
309
|
+
const writeLine = wsOutputPath ? (line) => appendFileSync(wsOutputPath, line + "\n") : (line) => process.stdout.write(line + "\n");
|
|
310
|
+
wsSink = {
|
|
311
|
+
path: wsPath,
|
|
312
|
+
onMessage(payload, info) {
|
|
313
|
+
writeLine(payload);
|
|
314
|
+
if (opts.debug) {
|
|
315
|
+
console.log(`[open-probe proxy] ws annotation from ${info.remoteAddress}`);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
onError(err) {
|
|
319
|
+
console.error("[open-probe proxy] ws error:", err.message);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
if (!opts.serverEndpoint && String(opts.transport) === "clipboard") {
|
|
323
|
+
config.transport = "server";
|
|
324
|
+
config.server = {
|
|
325
|
+
endpoint: `ws://${host}:${port}${wsPath}`,
|
|
326
|
+
protocol: "ws"
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const server = await createProxyServer({
|
|
331
|
+
target,
|
|
332
|
+
port,
|
|
333
|
+
host,
|
|
334
|
+
config,
|
|
335
|
+
...wsSink ? { wsSink } : {}
|
|
336
|
+
});
|
|
337
|
+
const url = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${server.address.port}`;
|
|
338
|
+
console.log(`\u2713 open-probe proxy listening on ${url}`);
|
|
339
|
+
console.log(` \u2192 upstream: ${target}`);
|
|
340
|
+
console.log(` \u2192 transport: ${config.transport}, output: ${config.outputLevel}`);
|
|
341
|
+
if (wsSink) {
|
|
342
|
+
console.log(` \u2192 ws sink: ws://${host}:${port}${wsPath}${opts.wsOutput ? ` \u2192 ${opts.wsOutput}` : " \u2192 stdout"}`);
|
|
343
|
+
}
|
|
344
|
+
const shutdown = async () => {
|
|
345
|
+
console.log("\nshutting down\u2026");
|
|
346
|
+
await server.close();
|
|
347
|
+
process.exit(0);
|
|
348
|
+
};
|
|
349
|
+
process.on("SIGINT", shutdown);
|
|
350
|
+
process.on("SIGTERM", shutdown);
|
|
351
|
+
});
|
|
352
|
+
cli.help();
|
|
353
|
+
cli.version(VERSION);
|
|
354
|
+
cli.parse();
|
|
355
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/injector.ts","../src/ws-sink.ts"],"sourcesContent":["import cac from 'cac';\nimport { appendFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport type { DeepPartial, OutputLevel, ProbeConfig, TransportName } from '@open-probe/shared';\nimport { createProxyServer } from './server.js';\nimport type { WsSinkOptions } from './ws-sink.js';\n\nconst VERSION = '0.1.0';\n\nconst cli = cac('open-probe');\n\ncli\n .command('[]', 'Start the open-probe reverse proxy')\n .option('-t, --target <url>', 'Upstream dev server URL (e.g. http://localhost:3000)')\n .option('-p, --port <port>', 'Proxy port', { default: '4000' })\n .option('--host <host>', 'Bind address', { default: '127.0.0.1' })\n .option('--transport <name>', 'clipboard | mcp | server', { default: 'clipboard' })\n .option('--output-level <level>', 'compact | standard | detailed | forensic', {\n default: 'standard',\n })\n .option('--mcp-endpoint <url>', 'MCP HTTP base URL', {\n default: 'http://127.0.0.1:3100',\n })\n .option('--server-endpoint <url>', 'Editor server endpoint (for transport=server)')\n .option('--ws', 'Enable WebSocket annotation sink on the proxy port')\n .option('--ws-path <path>', 'WebSocket upgrade path', { default: '/__open-probe/ws' })\n .option('--ws-output <file>', 'Append received annotation payloads to FILE (default: stdout)')\n .option('--debug', 'Enable verbose logging')\n .action(async opts => {\n const target = opts.target as string | undefined;\n if (!target) {\n console.error('error: --target is required');\n process.exit(1);\n }\n const port = Number(opts.port);\n const host = String(opts.host);\n\n const config: DeepPartial<ProbeConfig> = {\n transport: String(opts.transport) as TransportName,\n outputLevel: String(opts.outputLevel) as OutputLevel,\n mcp: { endpoint: String(opts.mcpEndpoint) },\n debug: Boolean(opts.debug),\n };\n if (opts.serverEndpoint) {\n config.server = { endpoint: String(opts.serverEndpoint), protocol: 'http' };\n }\n\n let wsSink: WsSinkOptions | undefined;\n const wsPath = String(opts.wsPath ?? '/__open-probe/ws');\n if (opts.ws) {\n const wsOutputPath = opts.wsOutput ? resolve(String(opts.wsOutput)) : null;\n const writeLine = wsOutputPath\n ? (line: string) => appendFileSync(wsOutputPath, line + '\\n')\n : (line: string) => process.stdout.write(line + '\\n');\n\n wsSink = {\n path: wsPath,\n onMessage(payload, info) {\n writeLine(payload);\n if (opts.debug) {\n // eslint-disable-next-line no-console\n console.log(`[open-probe proxy] ws annotation from ${info.remoteAddress}`);\n }\n },\n onError(err) {\n // eslint-disable-next-line no-console\n console.error('[open-probe proxy] ws error:', err.message);\n },\n };\n // Auto-wire transport when user did not configure it explicitly. The\n // default cac value for --transport is 'clipboard', which is rarely\n // intended together with --ws.\n if (!opts.serverEndpoint && String(opts.transport) === 'clipboard') {\n config.transport = 'server' as TransportName;\n config.server = {\n endpoint: `ws://${host}:${port}${wsPath}`,\n protocol: 'ws',\n };\n }\n }\n\n const server = await createProxyServer({\n target,\n port,\n host,\n config,\n ...(wsSink ? { wsSink } : {}),\n });\n const url = `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${server.address.port}`;\n // eslint-disable-next-line no-console\n console.log(`✓ open-probe proxy listening on ${url}`);\n // eslint-disable-next-line no-console\n console.log(` → upstream: ${target}`);\n // eslint-disable-next-line no-console\n console.log(` → transport: ${config.transport}, output: ${config.outputLevel}`);\n if (wsSink) {\n // eslint-disable-next-line no-console\n console.log(` → ws sink: ws://${host}:${port}${wsPath}${opts.wsOutput ? ` → ${opts.wsOutput}` : ' → stdout'}`);\n }\n\n const shutdown = async () => {\n // eslint-disable-next-line no-console\n console.log('\\nshutting down…');\n await server.close();\n process.exit(0);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n });\n\ncli.help();\ncli.version(VERSION);\ncli.parse();\n","import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport { request as httpRequest } from 'node:http';\nimport { request as httpsRequest } from 'node:https';\nimport { URL } from 'node:url';\nimport { promises as fs } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, resolve } from 'node:path';\nimport type { DeepPartial, ProbeConfig } from '@open-probe/shared';\nimport { buildInjection, injectIntoHtml } from './injector.js';\nimport { attachWsSink, type WsSinkOptions } from './ws-sink.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nexport interface ProxyOptions {\n target: string;\n port: number;\n host: string;\n /** Path served as the open-probe runtime */\n runtimePath?: string;\n config?: DeepPartial<ProbeConfig>;\n /** Override path to the IIFE bundle */\n scriptFile?: string;\n /**\n * Enable a WebSocket sink. When set, the proxy will accept WS upgrade\n * requests on `wsSink.path` and forward each text payload to\n * `wsSink.onMessage`. Useful for editors that don't support MCP.\n */\n wsSink?: WsSinkOptions;\n}\n\nexport type { WsSinkOptions } from './ws-sink.js';\n\nconst DEFAULT_RUNTIME_PATH = '/__open-probe/runtime.js';\n\nasync function locateScriptFile(override?: string): Promise<string> {\n if (override) return override;\n const candidates = [\n resolve(__dirname, '../../core/dist/index.global.js'),\n resolve(__dirname, '../../../core/dist/index.global.js'),\n resolve(process.cwd(), 'node_modules/@open-probe/core/dist/index.global.js'),\n ];\n for (const path of candidates) {\n try {\n await fs.access(path);\n return path;\n } catch {\n // try next\n }\n }\n throw new Error(\n 'Could not locate @open-probe/core IIFE bundle. Run `pnpm --filter @open-probe/core build` first.'\n );\n}\n\nfunction isHtmlAccept(req: IncomingMessage): boolean {\n const accept = req.headers.accept;\n if (!accept) return false;\n return accept.includes('text/html');\n}\n\nexport async function createProxyServer(options: ProxyOptions) {\n const targetUrl = new URL(options.target);\n const runtimePath = options.runtimePath ?? DEFAULT_RUNTIME_PATH;\n const scriptFile = await locateScriptFile(options.scriptFile);\n const injection = buildInjection(runtimePath, options.config ?? {});\n\n const requestFn = targetUrl.protocol === 'https:' ? httpsRequest : httpRequest;\n\n const server = createServer((req, res) => {\n handle(req, res).catch(err => {\n // eslint-disable-next-line no-console\n console.error('[open-probe proxy] error:', err);\n if (!res.headersSent) {\n res.writeHead(502, { 'Content-Type': 'text/plain' });\n }\n res.end(`open-probe proxy error: ${(err as Error).message}`);\n });\n });\n\n if (options.wsSink) {\n attachWsSink(server, options.wsSink);\n }\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = req.url ?? '/';\n\n if (url === runtimePath) {\n try {\n const code = await fs.readFile(scriptFile, 'utf-8');\n res.writeHead(200, {\n 'Content-Type': 'application/javascript; charset=utf-8',\n 'Cache-Control': 'no-cache',\n 'Access-Control-Allow-Origin': '*',\n });\n res.end(code);\n } catch (err) {\n res.writeHead(500);\n res.end(`Failed to read runtime: ${(err as Error).message}`);\n }\n return;\n }\n\n if (url === '/__open-probe/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ ok: true, target: options.target }));\n return;\n }\n\n proxyRequest(req, res);\n }\n\n function proxyRequest(req: IncomingMessage, res: ServerResponse): void {\n const headers = { ...req.headers };\n headers.host = targetUrl.host;\n delete headers['accept-encoding']; // We want to inspect HTML; disable compression.\n\n const upstream = requestFn(\n {\n protocol: targetUrl.protocol,\n host: targetUrl.hostname,\n port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),\n method: req.method,\n path: req.url,\n headers,\n },\n proxyRes => {\n const upstreamHeaders = { ...proxyRes.headers };\n delete upstreamHeaders['content-length'];\n delete upstreamHeaders['content-encoding'];\n\n const ct = String(proxyRes.headers['content-type'] ?? '');\n const wantsInjection = ct.includes('text/html') && isHtmlAccept(req);\n\n if (!wantsInjection) {\n res.writeHead(proxyRes.statusCode ?? 502, upstreamHeaders);\n proxyRes.pipe(res);\n return;\n }\n\n const chunks: Buffer[] = [];\n proxyRes.on('data', chunk => chunks.push(chunk as Buffer));\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks).toString('utf8');\n const injected = injectIntoHtml(body, injection);\n res.writeHead(proxyRes.statusCode ?? 200, upstreamHeaders);\n res.end(injected);\n });\n proxyRes.on('error', err => {\n if (!res.headersSent) res.writeHead(502);\n res.end(`upstream error: ${err.message}`);\n });\n }\n );\n\n upstream.on('error', err => {\n if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });\n res.end(`upstream connection failed: ${err.message}`);\n });\n\n req.pipe(upstream);\n }\n\n return new Promise<{\n close(): Promise<void>;\n address: { host: string; port: number };\n }>((resolveStart, rejectStart) => {\n server.once('error', rejectStart);\n server.listen(options.port, options.host, () => {\n const address = server.address();\n if (typeof address === 'object' && address) {\n resolveStart({\n address: { host: options.host, port: address.port },\n close: () =>\n new Promise<void>(done => {\n server.close(() => done());\n }),\n });\n } else {\n rejectStart(new Error('failed to bind proxy server'));\n }\n });\n });\n}\n","import type { DeepPartial, ProbeConfig } from '@open-probe/shared';\n\nconst HEAD_MATCH = /<head([^>]*)>/i;\n\nexport function buildInjection(scriptUrl: string, config: DeepPartial<ProbeConfig>): string {\n const json = JSON.stringify(config).replace(/</g, '\\\\u003c');\n return `<script>window.__OPEN_PROBE_CONFIG__=${json};</script><script src=\"${scriptUrl}\" defer></script>`;\n}\n\nexport function injectIntoHtml(html: string, snippet: string): string {\n if (!HEAD_MATCH.test(html)) {\n return html.replace(/<html([^>]*)>/i, `<html$1><head>${snippet}</head>`);\n }\n return html.replace(HEAD_MATCH, match => `${match}${snippet}`);\n}\n","import { createHash } from 'node:crypto';\nimport type { Server, IncomingMessage } from 'node:http';\nimport type { Duplex } from 'node:stream';\n\n/**\n * Minimal RFC 6455 WebSocket sink. Receives text frames from the browser,\n * decodes them to UTF-8, and forwards each payload to `onMessage`. Supports\n * masked client frames, payload lengths up to 4 GiB, ping → pong replies,\n * and graceful close. Server never originates messages, so encode logic is\n * limited to control frames.\n */\n\nconst GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';\n\nexport interface WsSinkOptions {\n path: string;\n onMessage(payload: string, info: { remoteAddress: string }): void;\n onError?(err: Error): void;\n}\n\nexport function attachWsSink(server: Server, opts: WsSinkOptions): void {\n server.on('upgrade', (req: IncomingMessage, socket: Duplex) => {\n if ((req.url ?? '/') !== opts.path) {\n socket.destroy();\n return;\n }\n const key = req.headers['sec-websocket-key'];\n if (typeof key !== 'string' || !key) {\n socket.destroy();\n return;\n }\n const accept = createHash('sha1').update(`${key}${GUID}`).digest('base64');\n socket.write(\n 'HTTP/1.1 101 Switching Protocols\\r\\n' +\n 'Upgrade: websocket\\r\\n' +\n 'Connection: Upgrade\\r\\n' +\n `Sec-WebSocket-Accept: ${accept}\\r\\n` +\n '\\r\\n'\n );\n const remote = req.socket.remoteAddress ?? '';\n pumpFrames(\n socket,\n text => opts.onMessage(text, { remoteAddress: remote }),\n err => opts.onError?.(err)\n );\n });\n}\n\nfunction pumpFrames(socket: Duplex, onText: (text: string) => void, onError: (e: Error) => void): void {\n let buffer = Buffer.alloc(0);\n let textChunks: Buffer[] = [];\n\n socket.on('data', (chunk: Buffer) => {\n buffer = (buffer.length ? Buffer.concat([buffer, chunk]) : chunk) as typeof buffer;\n while (buffer.length >= 2) {\n const frame = parseFrame(buffer);\n if (!frame) return;\n buffer = buffer.subarray(frame.consumed);\n\n if (frame.opcode === 0x0 || frame.opcode === 0x1) {\n textChunks.push(frame.payload);\n if (frame.fin) {\n const full = Buffer.concat(textChunks).toString('utf8');\n textChunks = [];\n try {\n onText(full);\n } catch (err) {\n onError(err as Error);\n }\n }\n } else if (frame.opcode === 0x8) {\n socket.end(encodeFrame(0x8, frame.payload));\n return;\n } else if (frame.opcode === 0x9) {\n try {\n socket.write(encodeFrame(0xa, frame.payload));\n } catch {\n /* ignore */\n }\n }\n // 0xA pong & 0x2 binary: ignored.\n }\n });\n socket.on('error', err => onError(err));\n}\n\ninterface ParsedFrame {\n fin: boolean;\n opcode: number;\n payload: Buffer;\n consumed: number;\n}\n\nfunction parseFrame(buffer: Buffer): ParsedFrame | null {\n if (buffer.length < 2) return null;\n const b0 = buffer[0]!;\n const b1 = buffer[1]!;\n const fin = (b0 & 0x80) !== 0;\n const opcode = b0 & 0x0f;\n const masked = (b1 & 0x80) !== 0;\n let payloadLen = b1 & 0x7f;\n let offset = 2;\n\n if (payloadLen === 126) {\n if (buffer.length < offset + 2) return null;\n payloadLen = buffer.readUInt16BE(offset);\n offset += 2;\n } else if (payloadLen === 127) {\n if (buffer.length < offset + 8) return null;\n const hi = buffer.readUInt32BE(offset);\n const lo = buffer.readUInt32BE(offset + 4);\n if (hi !== 0 || lo > 0x7fffffff) return null;\n payloadLen = lo;\n offset += 8;\n }\n\n let mask: Buffer | null = null;\n if (masked) {\n if (buffer.length < offset + 4) return null;\n mask = buffer.subarray(offset, offset + 4);\n offset += 4;\n }\n\n if (buffer.length < offset + payloadLen) return null;\n let payload = buffer.subarray(offset, offset + payloadLen);\n if (mask) {\n const out = Buffer.allocUnsafe(payloadLen);\n for (let i = 0; i < payloadLen; i++) out[i] = payload[i]! ^ mask[i & 3]!;\n payload = out;\n }\n return { fin, opcode, payload, consumed: offset + payloadLen };\n}\n\nfunction encodeFrame(opcode: number, payload: Buffer): Buffer {\n const len = payload.length;\n let header: Buffer;\n if (len < 126) {\n header = Buffer.from([0x80 | opcode, len]);\n } else if (len < 0x10000) {\n header = Buffer.alloc(4);\n header[0] = 0x80 | opcode;\n header[1] = 126;\n header.writeUInt16BE(len, 2);\n } else {\n header = Buffer.alloc(10);\n header[0] = 0x80 | opcode;\n header[1] = 127;\n header.writeUInt32BE(0, 2);\n header.writeUInt32BE(len, 6);\n }\n return Buffer.concat([header, payload]);\n}\n"],"mappings":"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","names":["resolve","resolve"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DeepPartial, ProbeConfig } from '@open-probe/shared';
|
|
2
|
+
import { Server } from 'node:http';
|
|
3
|
+
|
|
4
|
+
interface WsSinkOptions {
|
|
5
|
+
path: string;
|
|
6
|
+
onMessage(payload: string, info: {
|
|
7
|
+
remoteAddress: string;
|
|
8
|
+
}): void;
|
|
9
|
+
onError?(err: Error): void;
|
|
10
|
+
}
|
|
11
|
+
declare function attachWsSink(server: Server, opts: WsSinkOptions): void;
|
|
12
|
+
|
|
13
|
+
interface ProxyOptions {
|
|
14
|
+
target: string;
|
|
15
|
+
port: number;
|
|
16
|
+
host: string;
|
|
17
|
+
/** Path served as the open-probe runtime */
|
|
18
|
+
runtimePath?: string;
|
|
19
|
+
config?: DeepPartial<ProbeConfig>;
|
|
20
|
+
/** Override path to the IIFE bundle */
|
|
21
|
+
scriptFile?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Enable a WebSocket sink. When set, the proxy will accept WS upgrade
|
|
24
|
+
* requests on `wsSink.path` and forward each text payload to
|
|
25
|
+
* `wsSink.onMessage`. Useful for editors that don't support MCP.
|
|
26
|
+
*/
|
|
27
|
+
wsSink?: WsSinkOptions;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare function createProxyServer(options: ProxyOptions): Promise<{
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
address: {
|
|
33
|
+
host: string;
|
|
34
|
+
port: number;
|
|
35
|
+
};
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
declare function buildInjection(scriptUrl: string, config: DeepPartial<ProbeConfig>): string;
|
|
39
|
+
declare function injectIntoHtml(html: string, snippet: string): string;
|
|
40
|
+
|
|
41
|
+
export { type ProxyOptions, type WsSinkOptions, attachWsSink, buildInjection, createProxyServer, injectIntoHtml };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
import { createServer } from "http";
|
|
5
|
+
import { request as httpRequest } from "http";
|
|
6
|
+
import { request as httpsRequest } from "https";
|
|
7
|
+
import { URL } from "url";
|
|
8
|
+
import { promises as fs } from "fs";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { dirname, resolve } from "path";
|
|
11
|
+
|
|
12
|
+
// src/injector.ts
|
|
13
|
+
var HEAD_MATCH = /<head([^>]*)>/i;
|
|
14
|
+
function buildInjection(scriptUrl, config) {
|
|
15
|
+
const json = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
16
|
+
return `<script>window.__OPEN_PROBE_CONFIG__=${json};</script><script src="${scriptUrl}" defer></script>`;
|
|
17
|
+
}
|
|
18
|
+
function injectIntoHtml(html, snippet) {
|
|
19
|
+
if (!HEAD_MATCH.test(html)) {
|
|
20
|
+
return html.replace(/<html([^>]*)>/i, `<html$1><head>${snippet}</head>`);
|
|
21
|
+
}
|
|
22
|
+
return html.replace(HEAD_MATCH, (match) => `${match}${snippet}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/ws-sink.ts
|
|
26
|
+
import { createHash } from "crypto";
|
|
27
|
+
var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
28
|
+
function attachWsSink(server, opts) {
|
|
29
|
+
server.on("upgrade", (req, socket) => {
|
|
30
|
+
if ((req.url ?? "/") !== opts.path) {
|
|
31
|
+
socket.destroy();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const key = req.headers["sec-websocket-key"];
|
|
35
|
+
if (typeof key !== "string" || !key) {
|
|
36
|
+
socket.destroy();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const accept = createHash("sha1").update(`${key}${GUID}`).digest("base64");
|
|
40
|
+
socket.write(
|
|
41
|
+
`HTTP/1.1 101 Switching Protocols\r
|
|
42
|
+
Upgrade: websocket\r
|
|
43
|
+
Connection: Upgrade\r
|
|
44
|
+
Sec-WebSocket-Accept: ${accept}\r
|
|
45
|
+
\r
|
|
46
|
+
`
|
|
47
|
+
);
|
|
48
|
+
const remote = req.socket.remoteAddress ?? "";
|
|
49
|
+
pumpFrames(
|
|
50
|
+
socket,
|
|
51
|
+
(text) => opts.onMessage(text, { remoteAddress: remote }),
|
|
52
|
+
(err) => opts.onError?.(err)
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function pumpFrames(socket, onText, onError) {
|
|
57
|
+
let buffer = Buffer.alloc(0);
|
|
58
|
+
let textChunks = [];
|
|
59
|
+
socket.on("data", (chunk) => {
|
|
60
|
+
buffer = buffer.length ? Buffer.concat([buffer, chunk]) : chunk;
|
|
61
|
+
while (buffer.length >= 2) {
|
|
62
|
+
const frame = parseFrame(buffer);
|
|
63
|
+
if (!frame) return;
|
|
64
|
+
buffer = buffer.subarray(frame.consumed);
|
|
65
|
+
if (frame.opcode === 0 || frame.opcode === 1) {
|
|
66
|
+
textChunks.push(frame.payload);
|
|
67
|
+
if (frame.fin) {
|
|
68
|
+
const full = Buffer.concat(textChunks).toString("utf8");
|
|
69
|
+
textChunks = [];
|
|
70
|
+
try {
|
|
71
|
+
onText(full);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
onError(err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else if (frame.opcode === 8) {
|
|
77
|
+
socket.end(encodeFrame(8, frame.payload));
|
|
78
|
+
return;
|
|
79
|
+
} else if (frame.opcode === 9) {
|
|
80
|
+
try {
|
|
81
|
+
socket.write(encodeFrame(10, frame.payload));
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
socket.on("error", (err) => onError(err));
|
|
88
|
+
}
|
|
89
|
+
function parseFrame(buffer) {
|
|
90
|
+
if (buffer.length < 2) return null;
|
|
91
|
+
const b0 = buffer[0];
|
|
92
|
+
const b1 = buffer[1];
|
|
93
|
+
const fin = (b0 & 128) !== 0;
|
|
94
|
+
const opcode = b0 & 15;
|
|
95
|
+
const masked = (b1 & 128) !== 0;
|
|
96
|
+
let payloadLen = b1 & 127;
|
|
97
|
+
let offset = 2;
|
|
98
|
+
if (payloadLen === 126) {
|
|
99
|
+
if (buffer.length < offset + 2) return null;
|
|
100
|
+
payloadLen = buffer.readUInt16BE(offset);
|
|
101
|
+
offset += 2;
|
|
102
|
+
} else if (payloadLen === 127) {
|
|
103
|
+
if (buffer.length < offset + 8) return null;
|
|
104
|
+
const hi = buffer.readUInt32BE(offset);
|
|
105
|
+
const lo = buffer.readUInt32BE(offset + 4);
|
|
106
|
+
if (hi !== 0 || lo > 2147483647) return null;
|
|
107
|
+
payloadLen = lo;
|
|
108
|
+
offset += 8;
|
|
109
|
+
}
|
|
110
|
+
let mask = null;
|
|
111
|
+
if (masked) {
|
|
112
|
+
if (buffer.length < offset + 4) return null;
|
|
113
|
+
mask = buffer.subarray(offset, offset + 4);
|
|
114
|
+
offset += 4;
|
|
115
|
+
}
|
|
116
|
+
if (buffer.length < offset + payloadLen) return null;
|
|
117
|
+
let payload = buffer.subarray(offset, offset + payloadLen);
|
|
118
|
+
if (mask) {
|
|
119
|
+
const out = Buffer.allocUnsafe(payloadLen);
|
|
120
|
+
for (let i = 0; i < payloadLen; i++) out[i] = payload[i] ^ mask[i & 3];
|
|
121
|
+
payload = out;
|
|
122
|
+
}
|
|
123
|
+
return { fin, opcode, payload, consumed: offset + payloadLen };
|
|
124
|
+
}
|
|
125
|
+
function encodeFrame(opcode, payload) {
|
|
126
|
+
const len = payload.length;
|
|
127
|
+
let header;
|
|
128
|
+
if (len < 126) {
|
|
129
|
+
header = Buffer.from([128 | opcode, len]);
|
|
130
|
+
} else if (len < 65536) {
|
|
131
|
+
header = Buffer.alloc(4);
|
|
132
|
+
header[0] = 128 | opcode;
|
|
133
|
+
header[1] = 126;
|
|
134
|
+
header.writeUInt16BE(len, 2);
|
|
135
|
+
} else {
|
|
136
|
+
header = Buffer.alloc(10);
|
|
137
|
+
header[0] = 128 | opcode;
|
|
138
|
+
header[1] = 127;
|
|
139
|
+
header.writeUInt32BE(0, 2);
|
|
140
|
+
header.writeUInt32BE(len, 6);
|
|
141
|
+
}
|
|
142
|
+
return Buffer.concat([header, payload]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/server.ts
|
|
146
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
147
|
+
var __dirname = dirname(__filename);
|
|
148
|
+
var DEFAULT_RUNTIME_PATH = "/__open-probe/runtime.js";
|
|
149
|
+
async function locateScriptFile(override) {
|
|
150
|
+
if (override) return override;
|
|
151
|
+
const candidates = [
|
|
152
|
+
resolve(__dirname, "../../core/dist/index.global.js"),
|
|
153
|
+
resolve(__dirname, "../../../core/dist/index.global.js"),
|
|
154
|
+
resolve(process.cwd(), "node_modules/@open-probe/core/dist/index.global.js")
|
|
155
|
+
];
|
|
156
|
+
for (const path of candidates) {
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(path);
|
|
159
|
+
return path;
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw new Error(
|
|
164
|
+
"Could not locate @open-probe/core IIFE bundle. Run `pnpm --filter @open-probe/core build` first."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
function isHtmlAccept(req) {
|
|
168
|
+
const accept = req.headers.accept;
|
|
169
|
+
if (!accept) return false;
|
|
170
|
+
return accept.includes("text/html");
|
|
171
|
+
}
|
|
172
|
+
async function createProxyServer(options) {
|
|
173
|
+
const targetUrl = new URL(options.target);
|
|
174
|
+
const runtimePath = options.runtimePath ?? DEFAULT_RUNTIME_PATH;
|
|
175
|
+
const scriptFile = await locateScriptFile(options.scriptFile);
|
|
176
|
+
const injection = buildInjection(runtimePath, options.config ?? {});
|
|
177
|
+
const requestFn = targetUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
|
178
|
+
const server = createServer((req, res) => {
|
|
179
|
+
handle(req, res).catch((err) => {
|
|
180
|
+
console.error("[open-probe proxy] error:", err);
|
|
181
|
+
if (!res.headersSent) {
|
|
182
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
183
|
+
}
|
|
184
|
+
res.end(`open-probe proxy error: ${err.message}`);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
if (options.wsSink) {
|
|
188
|
+
attachWsSink(server, options.wsSink);
|
|
189
|
+
}
|
|
190
|
+
async function handle(req, res) {
|
|
191
|
+
const url = req.url ?? "/";
|
|
192
|
+
if (url === runtimePath) {
|
|
193
|
+
try {
|
|
194
|
+
const code = await fs.readFile(scriptFile, "utf-8");
|
|
195
|
+
res.writeHead(200, {
|
|
196
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
197
|
+
"Cache-Control": "no-cache",
|
|
198
|
+
"Access-Control-Allow-Origin": "*"
|
|
199
|
+
});
|
|
200
|
+
res.end(code);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
res.writeHead(500);
|
|
203
|
+
res.end(`Failed to read runtime: ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (url === "/__open-probe/health") {
|
|
208
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
209
|
+
res.end(JSON.stringify({ ok: true, target: options.target }));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
proxyRequest(req, res);
|
|
213
|
+
}
|
|
214
|
+
function proxyRequest(req, res) {
|
|
215
|
+
const headers = { ...req.headers };
|
|
216
|
+
headers.host = targetUrl.host;
|
|
217
|
+
delete headers["accept-encoding"];
|
|
218
|
+
const upstream = requestFn(
|
|
219
|
+
{
|
|
220
|
+
protocol: targetUrl.protocol,
|
|
221
|
+
host: targetUrl.hostname,
|
|
222
|
+
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
223
|
+
method: req.method,
|
|
224
|
+
path: req.url,
|
|
225
|
+
headers
|
|
226
|
+
},
|
|
227
|
+
(proxyRes) => {
|
|
228
|
+
const upstreamHeaders = { ...proxyRes.headers };
|
|
229
|
+
delete upstreamHeaders["content-length"];
|
|
230
|
+
delete upstreamHeaders["content-encoding"];
|
|
231
|
+
const ct = String(proxyRes.headers["content-type"] ?? "");
|
|
232
|
+
const wantsInjection = ct.includes("text/html") && isHtmlAccept(req);
|
|
233
|
+
if (!wantsInjection) {
|
|
234
|
+
res.writeHead(proxyRes.statusCode ?? 502, upstreamHeaders);
|
|
235
|
+
proxyRes.pipe(res);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const chunks = [];
|
|
239
|
+
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
240
|
+
proxyRes.on("end", () => {
|
|
241
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
242
|
+
const injected = injectIntoHtml(body, injection);
|
|
243
|
+
res.writeHead(proxyRes.statusCode ?? 200, upstreamHeaders);
|
|
244
|
+
res.end(injected);
|
|
245
|
+
});
|
|
246
|
+
proxyRes.on("error", (err) => {
|
|
247
|
+
if (!res.headersSent) res.writeHead(502);
|
|
248
|
+
res.end(`upstream error: ${err.message}`);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
upstream.on("error", (err) => {
|
|
253
|
+
if (!res.headersSent) res.writeHead(502, { "Content-Type": "text/plain" });
|
|
254
|
+
res.end(`upstream connection failed: ${err.message}`);
|
|
255
|
+
});
|
|
256
|
+
req.pipe(upstream);
|
|
257
|
+
}
|
|
258
|
+
return new Promise((resolveStart, rejectStart) => {
|
|
259
|
+
server.once("error", rejectStart);
|
|
260
|
+
server.listen(options.port, options.host, () => {
|
|
261
|
+
const address = server.address();
|
|
262
|
+
if (typeof address === "object" && address) {
|
|
263
|
+
resolveStart({
|
|
264
|
+
address: { host: options.host, port: address.port },
|
|
265
|
+
close: () => new Promise((done) => {
|
|
266
|
+
server.close(() => done());
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
rejectStart(new Error("failed to bind proxy server"));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
export {
|
|
276
|
+
attachWsSink,
|
|
277
|
+
buildInjection,
|
|
278
|
+
createProxyServer,
|
|
279
|
+
injectIntoHtml
|
|
280
|
+
};
|
|
281
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/injector.ts","../src/ws-sink.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport { request as httpRequest } from 'node:http';\nimport { request as httpsRequest } from 'node:https';\nimport { URL } from 'node:url';\nimport { promises as fs } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, resolve } from 'node:path';\nimport type { DeepPartial, ProbeConfig } from '@open-probe/shared';\nimport { buildInjection, injectIntoHtml } from './injector.js';\nimport { attachWsSink, type WsSinkOptions } from './ws-sink.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nexport interface ProxyOptions {\n target: string;\n port: number;\n host: string;\n /** Path served as the open-probe runtime */\n runtimePath?: string;\n config?: DeepPartial<ProbeConfig>;\n /** Override path to the IIFE bundle */\n scriptFile?: string;\n /**\n * Enable a WebSocket sink. When set, the proxy will accept WS upgrade\n * requests on `wsSink.path` and forward each text payload to\n * `wsSink.onMessage`. Useful for editors that don't support MCP.\n */\n wsSink?: WsSinkOptions;\n}\n\nexport type { WsSinkOptions } from './ws-sink.js';\n\nconst DEFAULT_RUNTIME_PATH = '/__open-probe/runtime.js';\n\nasync function locateScriptFile(override?: string): Promise<string> {\n if (override) return override;\n const candidates = [\n resolve(__dirname, '../../core/dist/index.global.js'),\n resolve(__dirname, '../../../core/dist/index.global.js'),\n resolve(process.cwd(), 'node_modules/@open-probe/core/dist/index.global.js'),\n ];\n for (const path of candidates) {\n try {\n await fs.access(path);\n return path;\n } catch {\n // try next\n }\n }\n throw new Error(\n 'Could not locate @open-probe/core IIFE bundle. Run `pnpm --filter @open-probe/core build` first.'\n );\n}\n\nfunction isHtmlAccept(req: IncomingMessage): boolean {\n const accept = req.headers.accept;\n if (!accept) return false;\n return accept.includes('text/html');\n}\n\nexport async function createProxyServer(options: ProxyOptions) {\n const targetUrl = new URL(options.target);\n const runtimePath = options.runtimePath ?? DEFAULT_RUNTIME_PATH;\n const scriptFile = await locateScriptFile(options.scriptFile);\n const injection = buildInjection(runtimePath, options.config ?? {});\n\n const requestFn = targetUrl.protocol === 'https:' ? httpsRequest : httpRequest;\n\n const server = createServer((req, res) => {\n handle(req, res).catch(err => {\n // eslint-disable-next-line no-console\n console.error('[open-probe proxy] error:', err);\n if (!res.headersSent) {\n res.writeHead(502, { 'Content-Type': 'text/plain' });\n }\n res.end(`open-probe proxy error: ${(err as Error).message}`);\n });\n });\n\n if (options.wsSink) {\n attachWsSink(server, options.wsSink);\n }\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = req.url ?? '/';\n\n if (url === runtimePath) {\n try {\n const code = await fs.readFile(scriptFile, 'utf-8');\n res.writeHead(200, {\n 'Content-Type': 'application/javascript; charset=utf-8',\n 'Cache-Control': 'no-cache',\n 'Access-Control-Allow-Origin': '*',\n });\n res.end(code);\n } catch (err) {\n res.writeHead(500);\n res.end(`Failed to read runtime: ${(err as Error).message}`);\n }\n return;\n }\n\n if (url === '/__open-probe/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ ok: true, target: options.target }));\n return;\n }\n\n proxyRequest(req, res);\n }\n\n function proxyRequest(req: IncomingMessage, res: ServerResponse): void {\n const headers = { ...req.headers };\n headers.host = targetUrl.host;\n delete headers['accept-encoding']; // We want to inspect HTML; disable compression.\n\n const upstream = requestFn(\n {\n protocol: targetUrl.protocol,\n host: targetUrl.hostname,\n port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),\n method: req.method,\n path: req.url,\n headers,\n },\n proxyRes => {\n const upstreamHeaders = { ...proxyRes.headers };\n delete upstreamHeaders['content-length'];\n delete upstreamHeaders['content-encoding'];\n\n const ct = String(proxyRes.headers['content-type'] ?? '');\n const wantsInjection = ct.includes('text/html') && isHtmlAccept(req);\n\n if (!wantsInjection) {\n res.writeHead(proxyRes.statusCode ?? 502, upstreamHeaders);\n proxyRes.pipe(res);\n return;\n }\n\n const chunks: Buffer[] = [];\n proxyRes.on('data', chunk => chunks.push(chunk as Buffer));\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks).toString('utf8');\n const injected = injectIntoHtml(body, injection);\n res.writeHead(proxyRes.statusCode ?? 200, upstreamHeaders);\n res.end(injected);\n });\n proxyRes.on('error', err => {\n if (!res.headersSent) res.writeHead(502);\n res.end(`upstream error: ${err.message}`);\n });\n }\n );\n\n upstream.on('error', err => {\n if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });\n res.end(`upstream connection failed: ${err.message}`);\n });\n\n req.pipe(upstream);\n }\n\n return new Promise<{\n close(): Promise<void>;\n address: { host: string; port: number };\n }>((resolveStart, rejectStart) => {\n server.once('error', rejectStart);\n server.listen(options.port, options.host, () => {\n const address = server.address();\n if (typeof address === 'object' && address) {\n resolveStart({\n address: { host: options.host, port: address.port },\n close: () =>\n new Promise<void>(done => {\n server.close(() => done());\n }),\n });\n } else {\n rejectStart(new Error('failed to bind proxy server'));\n }\n });\n });\n}\n","import type { DeepPartial, ProbeConfig } from '@open-probe/shared';\n\nconst HEAD_MATCH = /<head([^>]*)>/i;\n\nexport function buildInjection(scriptUrl: string, config: DeepPartial<ProbeConfig>): string {\n const json = JSON.stringify(config).replace(/</g, '\\\\u003c');\n return `<script>window.__OPEN_PROBE_CONFIG__=${json};</script><script src=\"${scriptUrl}\" defer></script>`;\n}\n\nexport function injectIntoHtml(html: string, snippet: string): string {\n if (!HEAD_MATCH.test(html)) {\n return html.replace(/<html([^>]*)>/i, `<html$1><head>${snippet}</head>`);\n }\n return html.replace(HEAD_MATCH, match => `${match}${snippet}`);\n}\n","import { createHash } from 'node:crypto';\nimport type { Server, IncomingMessage } from 'node:http';\nimport type { Duplex } from 'node:stream';\n\n/**\n * Minimal RFC 6455 WebSocket sink. Receives text frames from the browser,\n * decodes them to UTF-8, and forwards each payload to `onMessage`. Supports\n * masked client frames, payload lengths up to 4 GiB, ping → pong replies,\n * and graceful close. Server never originates messages, so encode logic is\n * limited to control frames.\n */\n\nconst GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';\n\nexport interface WsSinkOptions {\n path: string;\n onMessage(payload: string, info: { remoteAddress: string }): void;\n onError?(err: Error): void;\n}\n\nexport function attachWsSink(server: Server, opts: WsSinkOptions): void {\n server.on('upgrade', (req: IncomingMessage, socket: Duplex) => {\n if ((req.url ?? '/') !== opts.path) {\n socket.destroy();\n return;\n }\n const key = req.headers['sec-websocket-key'];\n if (typeof key !== 'string' || !key) {\n socket.destroy();\n return;\n }\n const accept = createHash('sha1').update(`${key}${GUID}`).digest('base64');\n socket.write(\n 'HTTP/1.1 101 Switching Protocols\\r\\n' +\n 'Upgrade: websocket\\r\\n' +\n 'Connection: Upgrade\\r\\n' +\n `Sec-WebSocket-Accept: ${accept}\\r\\n` +\n '\\r\\n'\n );\n const remote = req.socket.remoteAddress ?? '';\n pumpFrames(\n socket,\n text => opts.onMessage(text, { remoteAddress: remote }),\n err => opts.onError?.(err)\n );\n });\n}\n\nfunction pumpFrames(socket: Duplex, onText: (text: string) => void, onError: (e: Error) => void): void {\n let buffer = Buffer.alloc(0);\n let textChunks: Buffer[] = [];\n\n socket.on('data', (chunk: Buffer) => {\n buffer = (buffer.length ? Buffer.concat([buffer, chunk]) : chunk) as typeof buffer;\n while (buffer.length >= 2) {\n const frame = parseFrame(buffer);\n if (!frame) return;\n buffer = buffer.subarray(frame.consumed);\n\n if (frame.opcode === 0x0 || frame.opcode === 0x1) {\n textChunks.push(frame.payload);\n if (frame.fin) {\n const full = Buffer.concat(textChunks).toString('utf8');\n textChunks = [];\n try {\n onText(full);\n } catch (err) {\n onError(err as Error);\n }\n }\n } else if (frame.opcode === 0x8) {\n socket.end(encodeFrame(0x8, frame.payload));\n return;\n } else if (frame.opcode === 0x9) {\n try {\n socket.write(encodeFrame(0xa, frame.payload));\n } catch {\n /* ignore */\n }\n }\n // 0xA pong & 0x2 binary: ignored.\n }\n });\n socket.on('error', err => onError(err));\n}\n\ninterface ParsedFrame {\n fin: boolean;\n opcode: number;\n payload: Buffer;\n consumed: number;\n}\n\nfunction parseFrame(buffer: Buffer): ParsedFrame | null {\n if (buffer.length < 2) return null;\n const b0 = buffer[0]!;\n const b1 = buffer[1]!;\n const fin = (b0 & 0x80) !== 0;\n const opcode = b0 & 0x0f;\n const masked = (b1 & 0x80) !== 0;\n let payloadLen = b1 & 0x7f;\n let offset = 2;\n\n if (payloadLen === 126) {\n if (buffer.length < offset + 2) return null;\n payloadLen = buffer.readUInt16BE(offset);\n offset += 2;\n } else if (payloadLen === 127) {\n if (buffer.length < offset + 8) return null;\n const hi = buffer.readUInt32BE(offset);\n const lo = buffer.readUInt32BE(offset + 4);\n if (hi !== 0 || lo > 0x7fffffff) return null;\n payloadLen = lo;\n offset += 8;\n }\n\n let mask: Buffer | null = null;\n if (masked) {\n if (buffer.length < offset + 4) return null;\n mask = buffer.subarray(offset, offset + 4);\n offset += 4;\n }\n\n if (buffer.length < offset + payloadLen) return null;\n let payload = buffer.subarray(offset, offset + payloadLen);\n if (mask) {\n const out = Buffer.allocUnsafe(payloadLen);\n for (let i = 0; i < payloadLen; i++) out[i] = payload[i]! ^ mask[i & 3]!;\n payload = out;\n }\n return { fin, opcode, payload, consumed: offset + payloadLen };\n}\n\nfunction encodeFrame(opcode: number, payload: Buffer): Buffer {\n const len = payload.length;\n let header: Buffer;\n if (len < 126) {\n header = Buffer.from([0x80 | opcode, len]);\n } else if (len < 0x10000) {\n header = Buffer.alloc(4);\n header[0] = 0x80 | opcode;\n header[1] = 126;\n header.writeUInt16BE(len, 2);\n } else {\n header = Buffer.alloc(10);\n header[0] = 0x80 | opcode;\n header[1] = 127;\n header.writeUInt32BE(0, 2);\n header.writeUInt32BE(len, 6);\n }\n return Buffer.concat([header, payload]);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-probe/proxy",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Reverse proxy that injects open-probe runtime into any dev server.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "open-probe contributors",
|
|
7
|
+
"homepage": "https://github.com/wzc520pyfm/open-probe/tree/main/packages/proxy#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/wzc520pyfm/open-probe.git",
|
|
11
|
+
"directory": "packages/proxy"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/wzc520pyfm/open-probe/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"open-probe",
|
|
18
|
+
"reverse-proxy",
|
|
19
|
+
"injection",
|
|
20
|
+
"dev-server",
|
|
21
|
+
"html-rewrite"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"bin": {
|
|
28
|
+
"open-probe-proxy": "./dist/cli.js"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.18"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"cac": "^6.7.14",
|
|
50
|
+
"@open-probe/shared": "0.1.1",
|
|
51
|
+
"@open-probe/core": "0.1.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^22.10.5",
|
|
55
|
+
"tsup": "^8.3.5",
|
|
56
|
+
"typescript": "^5.7.3",
|
|
57
|
+
"vitest": "^2.1.8"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup",
|
|
61
|
+
"dev": "tsup --watch",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"start": "node dist/cli.js",
|
|
65
|
+
"clean": "rm -rf dist"
|
|
66
|
+
}
|
|
67
|
+
}
|