osborn 0.9.151 → 0.9.153
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/dist/index.js +60 -41
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { dirname, join } from 'node:path';
|
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
17
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
18
|
-
import
|
|
18
|
+
import httpProxy from 'http-proxy';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
// 0.9.71: createRequire for resolving package.json versions inside ESM
|
|
21
21
|
const __sdkVersionRequire = createRequire(import.meta.url);
|
|
@@ -282,11 +282,28 @@ let ideProxyEnabled = false;
|
|
|
282
282
|
// Bumped on every proxied HTTP request and WS upgrade so the idle watcher sees
|
|
283
283
|
// real editor activity rather than code-server log noise.
|
|
284
284
|
let ideLastProxiedActivity = 0;
|
|
285
|
+
// WebSocket proxy for code-server upgrades. Using http-proxy instead of a raw
|
|
286
|
+
// net.createConnection splice: the raw byte-splice causes an immediate 1006
|
|
287
|
+
// disconnect because it doesn't properly handle the HTTP/1.1 101 framing that
|
|
288
|
+
// code-server expects (confirmed: cloudflared works, raw splice does not).
|
|
289
|
+
const wsProxy = httpProxy.createProxyServer({ target: IDE_TARGET, ws: true, changeOrigin: true });
|
|
290
|
+
wsProxy.on('error', (err, _req, socket) => {
|
|
291
|
+
try {
|
|
292
|
+
socket?.destroy();
|
|
293
|
+
}
|
|
294
|
+
catch { }
|
|
295
|
+
});
|
|
285
296
|
// Per-session cookie token minted when code-server becomes ready. The proxy
|
|
286
297
|
// gate checks this token via the osborn_ide cookie — only requests carrying a
|
|
287
298
|
// valid cookie are forwarded, so a forgotten agent route can never be silently
|
|
288
299
|
// swallowed by the IDE proxy. Cleared in stopIde().
|
|
289
300
|
let ideSessionToken = null;
|
|
301
|
+
// Cancellation-generation guard for startCodeServer(). Incremented by stopIde()
|
|
302
|
+
// every time the IDE is torn down. startCodeServer() captures myGen at entry;
|
|
303
|
+
// if ideStartGeneration !== myGen when the poll resolves, the start has been
|
|
304
|
+
// superseded (stopIde ran mid-flight) and the result is discarded instead of
|
|
305
|
+
// re-enabling the proxy with no participant present.
|
|
306
|
+
let ideStartGeneration = 0;
|
|
290
307
|
// Helper: parse the osborn_ide cookie from a request and return true iff it
|
|
291
308
|
// matches the current ideSessionToken (and a token exists).
|
|
292
309
|
function hasValidIdeCookie(req) {
|
|
@@ -1528,7 +1545,7 @@ function startApiServer(workingDir, port) {
|
|
|
1528
1545
|
port: 8300,
|
|
1529
1546
|
path: targetUrl.pathname + (targetUrl.search || ''),
|
|
1530
1547
|
method: req.method,
|
|
1531
|
-
headers: { ...req.headers, host: '127.0.0.1:8300' },
|
|
1548
|
+
headers: { ...req.headers, host: '127.0.0.1:8300', 'x-forwarded-host': req.headers.host },
|
|
1532
1549
|
};
|
|
1533
1550
|
const proxyReq = httpRequest(options, (proxyRes) => {
|
|
1534
1551
|
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
|
@@ -1591,41 +1608,14 @@ function startApiServer(workingDir, port) {
|
|
|
1591
1608
|
const isAgentRoute = AGENT_ROUTE_PREFIXES.some(p => upgradePath === p || upgradePath.startsWith(p + '/'));
|
|
1592
1609
|
if (ideProxyEnabled && hasValidIdeCookie(req) && !isAgentRoute) {
|
|
1593
1610
|
ideLastProxiedActivity = Date.now();
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1597
|
-
//
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
const headerLines = [`${req.method} ${req.url} HTTP/1.1`];
|
|
1603
|
-
for (const [k, v] of Object.entries(req.headers)) {
|
|
1604
|
-
if (k.toLowerCase() === 'host')
|
|
1605
|
-
continue;
|
|
1606
|
-
if (Array.isArray(v)) {
|
|
1607
|
-
for (const vv of v)
|
|
1608
|
-
headerLines.push(`${k}: ${vv}`);
|
|
1609
|
-
}
|
|
1610
|
-
else if (v != null)
|
|
1611
|
-
headerLines.push(`${k}: ${v}`);
|
|
1612
|
-
}
|
|
1613
|
-
headerLines.push('host: 127.0.0.1:8300');
|
|
1614
|
-
conn.write(headerLines.join('\r\n') + '\r\n\r\n');
|
|
1615
|
-
if (head && head.length)
|
|
1616
|
-
conn.write(head);
|
|
1617
|
-
conn.pipe(socket);
|
|
1618
|
-
socket.pipe(conn);
|
|
1619
|
-
socket.resume();
|
|
1620
|
-
});
|
|
1621
|
-
conn.on('error', () => { try {
|
|
1622
|
-
socket.destroy();
|
|
1623
|
-
}
|
|
1624
|
-
catch { } });
|
|
1625
|
-
socket.on('error', () => { try {
|
|
1626
|
-
conn.destroy();
|
|
1627
|
-
}
|
|
1628
|
-
catch { } });
|
|
1611
|
+
// code-server's authenticateOrigin() compares Origin against the effective host
|
|
1612
|
+
// (X-Forwarded-Host first). changeOrigin:true rewrites Host→127.0.0.1:8300 while the
|
|
1613
|
+
// browser's Origin stays the public hostname, causing a 403 → WS 1006. Preserve the
|
|
1614
|
+
// real public host so the origin check matches.
|
|
1615
|
+
if (!req.headers['x-forwarded-host'] && req.headers.host) {
|
|
1616
|
+
req.headers['x-forwarded-host'] = req.headers.host;
|
|
1617
|
+
}
|
|
1618
|
+
wsProxy.ws(req, socket, head, { target: IDE_TARGET });
|
|
1629
1619
|
return;
|
|
1630
1620
|
}
|
|
1631
1621
|
socket.destroy();
|
|
@@ -2362,6 +2352,9 @@ async function main() {
|
|
|
2362
2352
|
};
|
|
2363
2353
|
// Kill code-server, disable proxy, clear interval, reset IDE state.
|
|
2364
2354
|
const stopIde = (reason) => {
|
|
2355
|
+
// Bump the generation counter so any in-flight startCodeServer() knows it
|
|
2356
|
+
// has been cancelled and must not re-enable the proxy or mint a new token.
|
|
2357
|
+
ideStartGeneration++;
|
|
2365
2358
|
ideProxyEnabled = false;
|
|
2366
2359
|
ideSessionToken = null;
|
|
2367
2360
|
ideStartInProgress = null;
|
|
@@ -2435,6 +2428,11 @@ async function main() {
|
|
|
2435
2428
|
// Fresh start: kill any stale proc (shouldn't normally exist here, but be safe).
|
|
2436
2429
|
if (ideCodeServerProc)
|
|
2437
2430
|
stopIde();
|
|
2431
|
+
// Capture the current generation AFTER any preceding stopIde() call so that
|
|
2432
|
+
// if stopIde() already ran (above), myGen reflects the incremented counter.
|
|
2433
|
+
// A concurrent stopIde() after this point will increment ideStartGeneration
|
|
2434
|
+
// beyond myGen, signalling cancellation to the poll loop and success branch.
|
|
2435
|
+
const myGen = ideStartGeneration;
|
|
2438
2436
|
let resolveFn;
|
|
2439
2437
|
let rejectFn;
|
|
2440
2438
|
ideStartInProgress = new Promise((res, rej) => { resolveFn = res; rejectFn = rej; });
|
|
@@ -2485,11 +2483,13 @@ async function main() {
|
|
|
2485
2483
|
await new Promise((res, rej) => {
|
|
2486
2484
|
let attempts = 0;
|
|
2487
2485
|
const poll = setInterval(() => {
|
|
2488
|
-
// Early-exit:
|
|
2489
|
-
//
|
|
2490
|
-
|
|
2486
|
+
// Early-exit: stopIde() was called while we were waiting (generation bumped),
|
|
2487
|
+
// OR the proc we spawned is no longer the active one for another reason.
|
|
2488
|
+
// Either way, cancel the poll immediately so we don't keep hitting 8300
|
|
2489
|
+
// for a boot that has already been invalidated.
|
|
2490
|
+
if (ideStartGeneration !== myGen || ideCodeServerProc !== codeServerProc) {
|
|
2491
2491
|
clearInterval(poll);
|
|
2492
|
-
rej(new Error('code-server
|
|
2492
|
+
rej(new Error('code-server start cancelled (stopIde ran mid-poll)'));
|
|
2493
2493
|
return;
|
|
2494
2494
|
}
|
|
2495
2495
|
attempts++;
|
|
@@ -2510,6 +2510,25 @@ async function main() {
|
|
|
2510
2510
|
}, 500);
|
|
2511
2511
|
});
|
|
2512
2512
|
console.log('✅ code-server ready on 8300');
|
|
2513
|
+
// Generation guard: check again right before enabling the proxy. The poll
|
|
2514
|
+
// already checks this on each tick, but there is a narrow window between the
|
|
2515
|
+
// last tick resolving and reaching here. If stopIde() ran in that window,
|
|
2516
|
+
// ideStartGeneration will have been incremented beyond myGen — we must NOT
|
|
2517
|
+
// enable the proxy or mint a token, and we must kill the orphan process so
|
|
2518
|
+
// no zombie code-server is left running with no participant present.
|
|
2519
|
+
if (ideStartGeneration !== myGen) {
|
|
2520
|
+
console.log('🖥️ IDE start cancelled (stopIde ran while code-server was booting) — killing orphan proc');
|
|
2521
|
+
try {
|
|
2522
|
+
codeServerProc.kill();
|
|
2523
|
+
}
|
|
2524
|
+
catch { }
|
|
2525
|
+
// ideCodeServerProc may already be null (stopIde cleared it); only null it
|
|
2526
|
+
// if it still points to our proc to avoid clobbering a newer start.
|
|
2527
|
+
if (ideCodeServerProc === codeServerProc)
|
|
2528
|
+
ideCodeServerProc = null;
|
|
2529
|
+
resolveFn(); // resolve (not reject) so the promise chain exits cleanly
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2513
2532
|
// Mint the session cookie token and enable the proxy. From this point the
|
|
2514
2533
|
// /editor route will 302+Set-Cookie and the fall-through proxy will only
|
|
2515
2534
|
// forward requests that carry the valid osborn_ide cookie.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osborn",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.153",
|
|
4
4
|
"description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"@vscode/ripgrep": "^1.17.1",
|
|
49
49
|
"diff": "^8.0.4",
|
|
50
50
|
"dotenv": "^16.4.0",
|
|
51
|
+
"http-proxy": "^1.18.1",
|
|
51
52
|
"livekit-server-sdk": "^2.15.0",
|
|
52
53
|
"minisearch": "^7.2.0",
|
|
53
54
|
"node-pty": "^1.1.0",
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
"zod": "^4.3.6"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
61
|
+
"@types/http-proxy": "^1.17.17",
|
|
60
62
|
"@types/node": "^20.0.0",
|
|
61
63
|
"@vitest/coverage-v8": "^4.0.18",
|
|
62
64
|
"typescript": "^5.0.0",
|