rnxsim 0.1.427 → 0.1.429

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 (59) hide show
  1. package/cli/commands/control.ts +17 -1
  2. package/cli/commands/flow.ts +5 -9
  3. package/cli/commands/inspect/core.ts +1 -0
  4. package/cli/outbound-endpoints.ts +5 -2
  5. package/cli/self-invocation.ts +24 -0
  6. package/detox/index.ts +10 -36
  7. package/dist-lib/agent-daemon-client.cjs +48 -24
  8. package/dist-lib/agent-events.cjs +1 -1
  9. package/dist-lib/agent-identity.cjs +1 -1
  10. package/dist-lib/agent-sessions.cjs +100 -76
  11. package/dist-lib/attached-projects.cjs +1 -1
  12. package/dist-lib/auth/shared-session.cjs +1 -1
  13. package/dist-lib/backend-origin.cjs +1 -1
  14. package/dist-lib/beta.cjs +1 -1
  15. package/dist-lib/beta.mjs +1 -1
  16. package/dist-lib/bridge-constants.cjs +1 -1
  17. package/dist-lib/bridge-contract-input.cjs +1 -1
  18. package/dist-lib/bridge-contract-input.mjs +1 -1
  19. package/dist-lib/bridge-contract.cjs +1 -1
  20. package/dist-lib/bridge-contract.mjs +1 -1
  21. package/dist-lib/capture-contract.cjs +1 -1
  22. package/dist-lib/capture-contract.mjs +1 -1
  23. package/dist-lib/cli-constants.cjs +1 -1
  24. package/dist-lib/cloud-contract.cjs +1 -1
  25. package/dist-lib/cloud-contract.mjs +1 -1
  26. package/dist-lib/cloud.cjs +1 -1
  27. package/dist-lib/cloud.mjs +1 -1
  28. package/dist-lib/config.cjs +1 -1
  29. package/dist-lib/detox/index.cjs +2 -25
  30. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  31. package/dist-lib/home-paths.cjs +1 -1
  32. package/dist-lib/host/bridge-host.cjs +324 -218
  33. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  34. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  36. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  37. package/dist-lib/host/websocket-proxy.cjs +162 -78
  38. package/dist-lib/index.cjs +1 -1
  39. package/dist-lib/jump-to-source-babel.cjs +1 -1
  40. package/dist-lib/menu.cjs +1 -1
  41. package/dist-lib/menu.mjs +1 -1
  42. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  43. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  44. package/dist-lib/metro-production-bundle.cjs +43 -24
  45. package/dist-lib/metro-production-bundle.mjs +43 -24
  46. package/dist-lib/metro.cjs +1 -1
  47. package/dist-lib/profiles.cjs +1 -1
  48. package/dist-lib/public-brand.cjs +1 -1
  49. package/dist-lib/react-native-host-modules.cjs +1 -1
  50. package/dist-lib/react-native-host-modules.mjs +1 -1
  51. package/dist-lib/render-mode.cjs +1 -1
  52. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  53. package/dist-lib/sdk.cjs +1 -1
  54. package/dist-lib/sdk.mjs +1 -1
  55. package/dist-lib/skills.cjs +452 -336
  56. package/dist-lib/vite.cjs +1 -1
  57. package/package.json +1 -1
  58. package/src/host/websocket-proxy.ts +197 -92
  59. package/src/metro-production-bundle.ts +55 -30
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.427 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.429 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -104,9 +104,9 @@ __export(bridge_host_exports, {
104
104
  module.exports = __toCommonJS(bridge_host_exports);
105
105
  var import_child_process3 = require("child_process");
106
106
  var import_fs2 = __toESM(require("fs"), 1);
107
- var import_http3 = require("http");
107
+ var import_http4 = require("http");
108
108
  var import_path2 = __toESM(require("path"), 1);
109
- var import_ws2 = require("ws");
109
+ var import_ws = require("ws");
110
110
 
111
111
  // scripts/dev-server-scanner.ts
112
112
  var import_child_process = require("child_process");
@@ -2054,23 +2054,47 @@ var RNX_RUNTIME_PRODUCT = {
2054
2054
  var rnxRuntime = createRuntimeDelivery(RNX_RUNTIME_PRODUCT);
2055
2055
 
2056
2056
  // src/host/agent-host.ts
2057
- var import_node_fs9 = __toESM(require("node:fs"), 1);
2058
- var import_node_path9 = __toESM(require("node:path"), 1);
2057
+ var import_node_fs10 = __toESM(require("node:fs"), 1);
2058
+ var import_node_path10 = __toESM(require("node:path"), 1);
2059
2059
 
2060
2060
  // src/agent-sessions.ts
2061
2061
  var import_node_child_process2 = require("node:child_process");
2062
2062
  var import_node_crypto4 = require("node:crypto");
2063
- var import_node_fs8 = __toESM(require("node:fs"), 1);
2064
- var import_node_path8 = __toESM(require("node:path"), 1);
2063
+ var import_node_fs9 = __toESM(require("node:fs"), 1);
2064
+ var import_node_path9 = __toESM(require("node:path"), 1);
2065
2065
  var import_node_readline = __toESM(require("node:readline"), 1);
2066
2066
 
2067
+ // cli/self-invocation.ts
2068
+ var import_node_fs7 = require("node:fs");
2069
+ var import_node_path7 = require("node:path");
2070
+ var import_node_url3 = require("node:url");
2071
+
2067
2072
  // cli/standalone.ts
2068
2073
  var IS_STANDALONE = typeof __SOOTSIM_STANDALONE__ !== "undefined" && __SOOTSIM_STANDALONE__;
2069
2074
 
2070
2075
  // cli/self-invocation.ts
2076
+ function sourceEntryPath() {
2077
+ try {
2078
+ const candidate = (0, import_node_path7.join)((0, import_node_path7.dirname)((0, import_node_url3.fileURLToPath)(__sootsim_import_meta_url)), "bin.ts");
2079
+ return (0, import_node_fs7.existsSync)(candidate) ? candidate : null;
2080
+ } catch {
2081
+ return null;
2082
+ }
2083
+ }
2084
+ function isCliEntry(path12) {
2085
+ const name2 = (0, import_node_path7.basename)(path12);
2086
+ return name2 === "bin.ts" || name2 === "bin.js";
2087
+ }
2071
2088
  function rnxSelfInvocation() {
2072
2089
  if (IS_STANDALONE) return { executable: process.execPath, prefixArgs: [] };
2073
2090
  const entry = process.argv[1];
2091
+ if (entry && isCliEntry(entry)) {
2092
+ return { executable: process.execPath, prefixArgs: [entry] };
2093
+ }
2094
+ const sourceEntry = sourceEntryPath();
2095
+ if (sourceEntry) {
2096
+ return { executable: process.execPath, prefixArgs: [sourceEntry] };
2097
+ }
2074
2098
  if (!entry) {
2075
2099
  throw new Error(`could not locate the ${rnxPublicBrand.commandName} entry script`);
2076
2100
  }
@@ -2118,8 +2142,8 @@ function encodeAgentPromptEnvelope(input) {
2118
2142
 
2119
2143
  // src/attached-projects.ts
2120
2144
  var import_node_crypto3 = require("node:crypto");
2121
- var import_node_fs7 = __toESM(require("node:fs"), 1);
2122
- var import_node_path7 = __toESM(require("node:path"), 1);
2145
+ var import_node_fs8 = __toESM(require("node:fs"), 1);
2146
+ var import_node_path8 = __toESM(require("node:path"), 1);
2123
2147
  var overrideDir = null;
2124
2148
  function userDataDir() {
2125
2149
  if (overrideDir) return overrideDir;
@@ -2131,7 +2155,7 @@ function getUserDataDir() {
2131
2155
  return userDataDir();
2132
2156
  }
2133
2157
  function storeFile() {
2134
- return import_node_path7.default.join(userDataDir(), "attached-projects.json");
2158
+ return import_node_path8.default.join(userDataDir(), "attached-projects.json");
2135
2159
  }
2136
2160
  function cloneEmpty() {
2137
2161
  return {
@@ -2145,7 +2169,7 @@ function loadStore() {
2145
2169
  const file = storeFile();
2146
2170
  let raw;
2147
2171
  try {
2148
- raw = import_node_fs7.default.readFileSync(file, "utf8");
2172
+ raw = import_node_fs8.default.readFileSync(file, "utf8");
2149
2173
  } catch (err) {
2150
2174
  if (err.code === "ENOENT") return cloneEmpty();
2151
2175
  throw err;
@@ -2162,7 +2186,7 @@ function loadStore() {
2162
2186
  } catch (err) {
2163
2187
  const quarantine = `${file}.corrupt-${Date.now()}`;
2164
2188
  try {
2165
- import_node_fs7.default.renameSync(file, quarantine);
2189
+ import_node_fs8.default.renameSync(file, quarantine);
2166
2190
  console.warn(
2167
2191
  `[sootsim] attached-projects.json was unparseable; quarantined to ${quarantine}. original error: ${err.message}`
2168
2192
  );
@@ -2173,16 +2197,16 @@ function loadStore() {
2173
2197
  }
2174
2198
  function writeStore(store) {
2175
2199
  const file = storeFile();
2176
- import_node_fs7.default.mkdirSync(import_node_path7.default.dirname(file), { recursive: true });
2200
+ import_node_fs8.default.mkdirSync(import_node_path8.default.dirname(file), { recursive: true });
2177
2201
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
2178
- const fd = import_node_fs7.default.openSync(tmp, "w", 384);
2202
+ const fd = import_node_fs8.default.openSync(tmp, "w", 384);
2179
2203
  try {
2180
- import_node_fs7.default.writeFileSync(fd, JSON.stringify(store, null, 2));
2181
- import_node_fs7.default.fsyncSync(fd);
2204
+ import_node_fs8.default.writeFileSync(fd, JSON.stringify(store, null, 2));
2205
+ import_node_fs8.default.fsyncSync(fd);
2182
2206
  } finally {
2183
- import_node_fs7.default.closeSync(fd);
2207
+ import_node_fs8.default.closeSync(fd);
2184
2208
  }
2185
- import_node_fs7.default.renameSync(tmp, file);
2209
+ import_node_fs8.default.renameSync(tmp, file);
2186
2210
  }
2187
2211
  function mutateStore(fn) {
2188
2212
  const store = loadStore();
@@ -2191,13 +2215,13 @@ function mutateStore(fn) {
2191
2215
  return store;
2192
2216
  }
2193
2217
  function projectIdForCwd(cwd) {
2194
- return (0, import_node_crypto3.createHash)("sha256").update(import_node_path7.default.resolve(cwd)).digest("hex").slice(0, 16);
2218
+ return (0, import_node_crypto3.createHash)("sha256").update(import_node_path8.default.resolve(cwd)).digest("hex").slice(0, 16);
2195
2219
  }
2196
2220
  function newSessionId() {
2197
2221
  return `s_${(0, import_node_crypto3.randomBytes)(10).toString("hex")}`;
2198
2222
  }
2199
2223
  function upsertProject(input) {
2200
- const cwd = import_node_path7.default.resolve(input.cwd);
2224
+ const cwd = import_node_path8.default.resolve(input.cwd);
2201
2225
  const id = projectIdForCwd(cwd);
2202
2226
  let result;
2203
2227
  mutateStore((store) => {
@@ -2223,7 +2247,7 @@ function upsertProject(input) {
2223
2247
  const now = Date.now();
2224
2248
  const created = {
2225
2249
  id,
2226
- name: input.name ?? import_node_path7.default.basename(cwd),
2250
+ name: input.name ?? import_node_path8.default.basename(cwd),
2227
2251
  cwd,
2228
2252
  repoRoot: input.repoRoot,
2229
2253
  sourceRoots: input.sourceRoots ?? [cwd],
@@ -2369,8 +2393,8 @@ async function seedFromDemoAppRegistry() {
2369
2393
  if (apps.length === 0) return;
2370
2394
  mutateStore((store) => {
2371
2395
  for (const app of apps) {
2372
- if (!import_node_fs7.default.existsSync(app.dir)) continue;
2373
- const cwd = import_node_path7.default.resolve(app.dir);
2396
+ if (!import_node_fs8.default.existsSync(app.dir)) continue;
2397
+ const cwd = import_node_path8.default.resolve(app.dir);
2374
2398
  const id = projectIdForCwd(cwd);
2375
2399
  if (store.attachedProjects.some((p) => p.id === id)) continue;
2376
2400
  const now = Date.now();
@@ -2394,16 +2418,16 @@ async function seedFromDemoAppRegistry() {
2394
2418
 
2395
2419
  // src/agent-sessions.ts
2396
2420
  function sessionDir(sessionId) {
2397
- return import_node_path8.default.join(getUserDataDir(), "sessions", sessionId);
2421
+ return import_node_path9.default.join(getUserDataDir(), "sessions", sessionId);
2398
2422
  }
2399
2423
  function promptFifoPath(sessionId) {
2400
- return import_node_path8.default.join(sessionDir(sessionId), "prompt.in");
2424
+ return import_node_path9.default.join(sessionDir(sessionId), "prompt.in");
2401
2425
  }
2402
2426
  function eventsFifoPath(sessionId) {
2403
- return import_node_path8.default.join(sessionDir(sessionId), "events.out");
2427
+ return import_node_path9.default.join(sessionDir(sessionId), "events.out");
2404
2428
  }
2405
2429
  function transcriptPath(sessionId) {
2406
- return import_node_path8.default.join(getUserDataDir(), "transcripts", `${sessionId}.log`);
2430
+ return import_node_path9.default.join(getUserDataDir(), "transcripts", `${sessionId}.log`);
2407
2431
  }
2408
2432
  function pidIsAlive(pid, sessionId) {
2409
2433
  if (!pid) return false;
@@ -2413,7 +2437,7 @@ function pidIsAlive(pid, sessionId) {
2413
2437
  return false;
2414
2438
  }
2415
2439
  if (sessionId) {
2416
- if (!import_node_fs8.default.existsSync(sessionDir(sessionId))) return false;
2440
+ if (!import_node_fs9.default.existsSync(sessionDir(sessionId))) return false;
2417
2441
  }
2418
2442
  return true;
2419
2443
  }
@@ -2426,11 +2450,11 @@ function resolveSootsimInvocation() {
2426
2450
  const resourcesPath = process.resourcesPath;
2427
2451
  if (resourcesPath) {
2428
2452
  const candidates = [
2429
- import_node_path8.default.join(resourcesPath, "bin", "sootsim"),
2430
- import_node_path8.default.join(resourcesPath, "bin", `sootsim-${process.platform}-${process.arch}`)
2453
+ import_node_path9.default.join(resourcesPath, "bin", "sootsim"),
2454
+ import_node_path9.default.join(resourcesPath, "bin", `sootsim-${process.platform}-${process.arch}`)
2431
2455
  ];
2432
2456
  for (const c of candidates) {
2433
- if (import_node_fs8.default.existsSync(c)) return { executable: c, prefixArgs: [] };
2457
+ if (import_node_fs9.default.existsSync(c)) return { executable: c, prefixArgs: [] };
2434
2458
  }
2435
2459
  }
2436
2460
  }
@@ -2449,15 +2473,15 @@ function tryWorkspaceSootsim() {
2449
2473
  const sootsimDir = resolveSootsimPackageDir();
2450
2474
  if (!sootsimDir) return null;
2451
2475
  const binaryName = `rnx-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`;
2452
- const distBinary = import_node_path8.default.join(sootsimDir, "dist-bin", binaryName);
2453
- if (import_node_fs8.default.existsSync(distBinary)) return { executable: distBinary, prefixArgs: [] };
2454
- const distBin = import_node_path8.default.join(sootsimDir, "dist-cli", "bin.js");
2455
- if (import_node_fs8.default.existsSync(distBin)) {
2476
+ const distBinary = import_node_path9.default.join(sootsimDir, "dist-bin", binaryName);
2477
+ if (import_node_fs9.default.existsSync(distBinary)) return { executable: distBinary, prefixArgs: [] };
2478
+ const distBin = import_node_path9.default.join(sootsimDir, "dist-cli", "bin.js");
2479
+ if (import_node_fs9.default.existsSync(distBin)) {
2456
2480
  try {
2457
- const src = import_node_path8.default.join(sootsimDir, "cli", "commands", "agent-wrapper.ts");
2458
- if (import_node_fs8.default.existsSync(src)) {
2459
- const srcMtime = import_node_fs8.default.statSync(src).mtimeMs;
2460
- const buildMtime = import_node_fs8.default.statSync(distBin).mtimeMs;
2481
+ const src = import_node_path9.default.join(sootsimDir, "cli", "commands", "agent-wrapper.ts");
2482
+ if (import_node_fs9.default.existsSync(src)) {
2483
+ const srcMtime = import_node_fs9.default.statSync(src).mtimeMs;
2484
+ const buildMtime = import_node_fs9.default.statSync(distBin).mtimeMs;
2461
2485
  if (buildMtime < srcMtime) {
2462
2486
  console.warn(
2463
2487
  `[rnx] dist-cli/bin.js is older than agent-wrapper.ts \u2014 rebuild with \`bun run --cwd packages/sootsim build:cli\`.`
@@ -2476,22 +2500,22 @@ function tryWorkspaceSootsim() {
2476
2500
  function resolveSootsimPackageDir() {
2477
2501
  try {
2478
2502
  const resolved = require.resolve(`${rnxPublicBrand.packageName}/package.json`);
2479
- return import_node_path8.default.dirname(resolved);
2503
+ return import_node_path9.default.dirname(resolved);
2480
2504
  } catch {
2481
2505
  }
2482
2506
  const here = fileFromImportMeta();
2483
2507
  if (!here) return null;
2484
- let cur = import_node_path8.default.dirname(here);
2508
+ let cur = import_node_path9.default.dirname(here);
2485
2509
  for (let i = 0; i < 8; i++) {
2486
- const pkg = import_node_path8.default.join(cur, "package.json");
2510
+ const pkg = import_node_path9.default.join(cur, "package.json");
2487
2511
  try {
2488
- if (import_node_fs8.default.existsSync(pkg)) {
2489
- const parsed = JSON.parse(import_node_fs8.default.readFileSync(pkg, "utf8"));
2512
+ if (import_node_fs9.default.existsSync(pkg)) {
2513
+ const parsed = JSON.parse(import_node_fs9.default.readFileSync(pkg, "utf8"));
2490
2514
  if (parsed.name === rnxPublicBrand.packageName) return cur;
2491
2515
  }
2492
2516
  } catch {
2493
2517
  }
2494
- const parent = import_node_path8.default.dirname(cur);
2518
+ const parent = import_node_path9.default.dirname(cur);
2495
2519
  if (parent === cur) break;
2496
2520
  cur = parent;
2497
2521
  }
@@ -2507,28 +2531,28 @@ function fileFromImportMeta() {
2507
2531
  }
2508
2532
  }
2509
2533
  async function withStartLock(projectId, provider, fn) {
2510
- const lockDir = import_node_path8.default.join(getUserDataDir(), "locks");
2511
- import_node_fs8.default.mkdirSync(lockDir, { recursive: true });
2534
+ const lockDir = import_node_path9.default.join(getUserDataDir(), "locks");
2535
+ import_node_fs9.default.mkdirSync(lockDir, { recursive: true });
2512
2536
  try {
2513
- import_node_fs8.default.chmodSync(lockDir, 448);
2537
+ import_node_fs9.default.chmodSync(lockDir, 448);
2514
2538
  } catch {
2515
2539
  }
2516
- const lockPath = import_node_path8.default.join(lockDir, `start-${projectId}-${provider}.lock`);
2540
+ const lockPath = import_node_path9.default.join(lockDir, `start-${projectId}-${provider}.lock`);
2517
2541
  const deadline = Date.now() + 4e3;
2518
2542
  let fd = null;
2519
2543
  while (fd === null) {
2520
2544
  try {
2521
- fd = import_node_fs8.default.openSync(
2545
+ fd = import_node_fs9.default.openSync(
2522
2546
  lockPath,
2523
- import_node_fs8.constants.O_WRONLY | import_node_fs8.constants.O_CREAT | import_node_fs8.constants.O_EXCL,
2547
+ import_node_fs9.constants.O_WRONLY | import_node_fs9.constants.O_CREAT | import_node_fs9.constants.O_EXCL,
2524
2548
  384
2525
2549
  );
2526
2550
  } catch (err) {
2527
2551
  if (err.code !== "EEXIST") throw err;
2528
2552
  try {
2529
- const stale = Number(import_node_fs8.default.readFileSync(lockPath, "utf8").trim());
2553
+ const stale = Number(import_node_fs9.default.readFileSync(lockPath, "utf8").trim());
2530
2554
  if (stale && !isProcessAlive(stale)) {
2531
- import_node_fs8.default.unlinkSync(lockPath);
2555
+ import_node_fs9.default.unlinkSync(lockPath);
2532
2556
  continue;
2533
2557
  }
2534
2558
  } catch {
@@ -2542,15 +2566,15 @@ async function withStartLock(projectId, provider, fn) {
2542
2566
  }
2543
2567
  }
2544
2568
  try {
2545
- import_node_fs8.default.writeFileSync(fd, String(process.pid));
2569
+ import_node_fs9.default.writeFileSync(fd, String(process.pid));
2546
2570
  return await fn();
2547
2571
  } finally {
2548
2572
  try {
2549
- import_node_fs8.default.closeSync(fd);
2573
+ import_node_fs9.default.closeSync(fd);
2550
2574
  } catch {
2551
2575
  }
2552
2576
  try {
2553
- import_node_fs8.default.unlinkSync(lockPath);
2577
+ import_node_fs9.default.unlinkSync(lockPath);
2554
2578
  } catch {
2555
2579
  }
2556
2580
  }
@@ -2564,25 +2588,25 @@ function isProcessAlive(pid) {
2564
2588
  }
2565
2589
  }
2566
2590
  function mkfifoSync(p) {
2567
- const parent = import_node_path8.default.dirname(p);
2568
- import_node_fs8.default.mkdirSync(parent, { recursive: true });
2591
+ const parent = import_node_path9.default.dirname(p);
2592
+ import_node_fs9.default.mkdirSync(parent, { recursive: true });
2569
2593
  try {
2570
- import_node_fs8.default.chmodSync(parent, 448);
2594
+ import_node_fs9.default.chmodSync(parent, 448);
2571
2595
  } catch {
2572
2596
  }
2573
- if (import_node_fs8.default.existsSync(p)) {
2597
+ if (import_node_fs9.default.existsSync(p)) {
2574
2598
  try {
2575
- const stat2 = import_node_fs8.default.statSync(p);
2599
+ const stat2 = import_node_fs9.default.statSync(p);
2576
2600
  if (stat2.isFIFO()) {
2577
2601
  try {
2578
- import_node_fs8.default.chmodSync(p, 384);
2602
+ import_node_fs9.default.chmodSync(p, 384);
2579
2603
  } catch {
2580
2604
  }
2581
2605
  return;
2582
2606
  }
2583
- import_node_fs8.default.unlinkSync(p);
2607
+ import_node_fs9.default.unlinkSync(p);
2584
2608
  } catch {
2585
- import_node_fs8.default.unlinkSync(p);
2609
+ import_node_fs9.default.unlinkSync(p);
2586
2610
  }
2587
2611
  }
2588
2612
  const result = (0, import_node_child_process2.spawnSync)("mkfifo", ["-m", "600", p]);
@@ -2629,10 +2653,10 @@ async function startSession(opts) {
2629
2653
  const transcript = transcriptPath(session.id);
2630
2654
  mkfifoSync(promptIn);
2631
2655
  mkfifoSync(eventsOut);
2632
- const transcriptDir = import_node_path8.default.dirname(transcript);
2633
- import_node_fs8.default.mkdirSync(transcriptDir, { recursive: true });
2656
+ const transcriptDir = import_node_path9.default.dirname(transcript);
2657
+ import_node_fs9.default.mkdirSync(transcriptDir, { recursive: true });
2634
2658
  try {
2635
- import_node_fs8.default.chmodSync(transcriptDir, 448);
2659
+ import_node_fs9.default.chmodSync(transcriptDir, 448);
2636
2660
  } catch {
2637
2661
  }
2638
2662
  const { executable, prefixArgs } = resolveSootsimInvocation();
@@ -2683,7 +2707,7 @@ async function startSession(opts) {
2683
2707
  }
2684
2708
  }
2685
2709
  try {
2686
- import_node_fs8.default.rmSync(sessionDir(session.id), { recursive: true, force: true });
2710
+ import_node_fs9.default.rmSync(sessionDir(session.id), { recursive: true, force: true });
2687
2711
  } catch {
2688
2712
  }
2689
2713
  updateSessionStatus(session.id, { status: "ended" });
@@ -2711,18 +2735,18 @@ async function sendPrompt(sessionId, prompt) {
2711
2735
  );
2712
2736
  }
2713
2737
  const fifo = promptFifoPath(sessionId);
2714
- if (!import_node_fs8.default.existsSync(fifo)) {
2738
+ if (!import_node_fs9.default.existsSync(fifo)) {
2715
2739
  throw new AgentSessionError("NO_FIFO", `prompt FIFO missing: ${fifo}`);
2716
2740
  }
2717
- const fd = import_node_fs8.default.openSync(fifo, import_node_fs8.constants.O_WRONLY);
2741
+ const fd = import_node_fs9.default.openSync(fifo, import_node_fs9.constants.O_WRONLY);
2718
2742
  try {
2719
2743
  const wireText = encodeAgentPromptEnvelope(prompt);
2720
2744
  if (!wireText) {
2721
2745
  throw new AgentSessionError("EMPTY_PROMPT", "prompt text is empty");
2722
2746
  }
2723
- import_node_fs8.default.writeSync(fd, wireText + "\n");
2747
+ import_node_fs9.default.writeSync(fd, wireText + "\n");
2724
2748
  } finally {
2725
- import_node_fs8.default.closeSync(fd);
2749
+ import_node_fs9.default.closeSync(fd);
2726
2750
  }
2727
2751
  updateSessionStatus(sessionId, {
2728
2752
  lastPrompt: prompt.displayText ?? prompt.text,
@@ -2744,7 +2768,7 @@ async function endSession(sessionId) {
2744
2768
  const base = getUserDataDir();
2745
2769
  if (dir.startsWith(base)) {
2746
2770
  try {
2747
- import_node_fs8.default.rmSync(dir, { recursive: true, force: true });
2771
+ import_node_fs9.default.rmSync(dir, { recursive: true, force: true });
2748
2772
  } catch {
2749
2773
  }
2750
2774
  }
@@ -2752,11 +2776,11 @@ async function endSession(sessionId) {
2752
2776
  }
2753
2777
  function subscribeEvents(sessionId, onEvent) {
2754
2778
  const fifo = eventsFifoPath(sessionId);
2755
- if (!import_node_fs8.default.existsSync(fifo)) {
2779
+ if (!import_node_fs9.default.existsSync(fifo)) {
2756
2780
  throw new AgentSessionError("NO_FIFO", `events FIFO missing: ${fifo}`);
2757
2781
  }
2758
- const fd = import_node_fs8.default.openSync(fifo, import_node_fs8.constants.O_RDWR);
2759
- const stream = import_node_fs8.default.createReadStream("", { fd, autoClose: true });
2782
+ const fd = import_node_fs9.default.openSync(fifo, import_node_fs9.constants.O_RDWR);
2783
+ const stream = import_node_fs9.default.createReadStream("", { fd, autoClose: true });
2760
2784
  const rl = import_node_readline.default.createInterface({ input: stream, crlfDelay: Infinity });
2761
2785
  rl.on("line", (line) => {
2762
2786
  const event = parseAgentEventLine(line);
@@ -2767,7 +2791,7 @@ function subscribeEvents(sessionId, onEvent) {
2767
2791
  if (closed) return;
2768
2792
  closed = true;
2769
2793
  try {
2770
- import_node_fs8.default.writeSync(fd, "\n");
2794
+ import_node_fs9.default.writeSync(fd, "\n");
2771
2795
  } catch {
2772
2796
  }
2773
2797
  try {
@@ -2781,7 +2805,7 @@ function subscribeEvents(sessionId, onEvent) {
2781
2805
  };
2782
2806
  }
2783
2807
  async function waitForFirstEvent(fifo, predicate, timeoutMs) {
2784
- const fd = import_node_fs8.default.openSync(fifo, import_node_fs8.constants.O_RDWR | import_node_fs8.constants.O_NONBLOCK);
2808
+ const fd = import_node_fs9.default.openSync(fifo, import_node_fs9.constants.O_RDWR | import_node_fs9.constants.O_NONBLOCK);
2785
2809
  const buf = Buffer.alloc(8192);
2786
2810
  let leftover = "";
2787
2811
  const deadline = Date.now() + timeoutMs;
@@ -2789,7 +2813,7 @@ async function waitForFirstEvent(fifo, predicate, timeoutMs) {
2789
2813
  while (Date.now() < deadline) {
2790
2814
  let n = 0;
2791
2815
  try {
2792
- n = import_node_fs8.default.readSync(fd, buf, 0, buf.length, null);
2816
+ n = import_node_fs9.default.readSync(fd, buf, 0, buf.length, null);
2793
2817
  } catch (err) {
2794
2818
  if (err.code !== "EAGAIN") throw err;
2795
2819
  n = 0;
@@ -2809,7 +2833,7 @@ async function waitForFirstEvent(fifo, predicate, timeoutMs) {
2809
2833
  }
2810
2834
  return null;
2811
2835
  } finally {
2812
- import_node_fs8.default.closeSync(fd);
2836
+ import_node_fs9.default.closeSync(fd);
2813
2837
  }
2814
2838
  }
2815
2839
 
@@ -3087,7 +3111,7 @@ var AgentHost = class {
3087
3111
  );
3088
3112
  const project = upsertProject({
3089
3113
  cwd: match.cwd,
3090
- name: match.projectName ?? import_node_path9.default.basename(match.cwd),
3114
+ name: match.projectName ?? import_node_path10.default.basename(match.cwd),
3091
3115
  preferredProvider: input.provider ?? existing?.preferredProvider,
3092
3116
  sourceRoots: existing?.sourceRoots ?? [match.cwd],
3093
3117
  knownBundleUrls,
@@ -3098,18 +3122,18 @@ var AgentHost = class {
3098
3122
  }
3099
3123
  getTranscript(sessionId) {
3100
3124
  const p = transcriptPath(sessionId);
3101
- if (!import_node_fs9.default.existsSync(p)) {
3125
+ if (!import_node_fs10.default.existsSync(p)) {
3102
3126
  return { error: "transcript not found", code: "NO_TRANSCRIPT" };
3103
3127
  }
3104
- return import_node_fs9.default.readFileSync(p, "utf8");
3128
+ return import_node_fs10.default.readFileSync(p, "utf8");
3105
3129
  }
3106
3130
  getPaths() {
3107
3131
  const dir = getUserDataDir();
3108
3132
  return {
3109
3133
  userDataDir: dir,
3110
- storeFile: import_node_path9.default.join(dir, "attached-projects.json"),
3111
- sessionsDir: import_node_path9.default.join(dir, "sessions"),
3112
- transcriptsDir: import_node_path9.default.join(dir, "transcripts")
3134
+ storeFile: import_node_path10.default.join(dir, "attached-projects.json"),
3135
+ sessionsDir: import_node_path10.default.join(dir, "sessions"),
3136
+ transcriptsDir: import_node_path10.default.join(dir, "transcripts")
3113
3137
  };
3114
3138
  }
3115
3139
  // --- subscription management ---
@@ -3812,8 +3836,8 @@ async function openUrl(url, options = {}) {
3812
3836
  }
3813
3837
 
3814
3838
  // src/host/plane-host.ts
3815
- var import_node_fs11 = __toESM(require("node:fs"), 1);
3816
- var import_node_path11 = __toESM(require("node:path"), 1);
3839
+ var import_node_fs12 = __toESM(require("node:fs"), 1);
3840
+ var import_node_path12 = __toESM(require("node:path"), 1);
3817
3841
 
3818
3842
  // ../rnx-box/src/planeProtocol.ts
3819
3843
  var PLANE_FLAG_READ_ONLY = 1;
@@ -3893,8 +3917,8 @@ async function dispatchPlaneCommand(plane, type, msg, mutationOptions) {
3893
3917
 
3894
3918
  // cli/commands/box/checkout-plane.ts
3895
3919
  var import_node_child_process4 = require("node:child_process");
3896
- var import_node_fs10 = __toESM(require("node:fs"), 1);
3897
- var import_node_path10 = __toESM(require("node:path"), 1);
3920
+ var import_node_fs11 = __toESM(require("node:fs"), 1);
3921
+ var import_node_path11 = __toESM(require("node:path"), 1);
3898
3922
  var CheckoutRequiresGitError = class extends Error {
3899
3923
  };
3900
3924
  var BINARY_SNIFF_BYTES = 8192;
@@ -3905,14 +3929,14 @@ function looksBinary(filePath, size) {
3905
3929
  if (size === 0) return false;
3906
3930
  let fd;
3907
3931
  try {
3908
- fd = import_node_fs10.default.openSync(filePath, "r");
3932
+ fd = import_node_fs11.default.openSync(filePath, "r");
3909
3933
  const buffer = Buffer.alloc(Math.min(BINARY_SNIFF_BYTES, size));
3910
- const read = import_node_fs10.default.readSync(fd, buffer, 0, buffer.length, 0);
3934
+ const read = import_node_fs11.default.readSync(fd, buffer, 0, buffer.length, 0);
3911
3935
  return buffer.subarray(0, read).includes(0);
3912
3936
  } catch {
3913
3937
  return false;
3914
3938
  } finally {
3915
- if (fd !== void 0) import_node_fs10.default.closeSync(fd);
3939
+ if (fd !== void 0) import_node_fs11.default.closeSync(fd);
3916
3940
  }
3917
3941
  }
3918
3942
  var CheckoutFilePlane = class {
@@ -3925,7 +3949,7 @@ var CheckoutFilePlane = class {
3925
3949
  listTimeoutMs;
3926
3950
  trackedOnly;
3927
3951
  constructor(root, options = {}) {
3928
- this.root = import_node_path10.default.resolve(root);
3952
+ this.root = import_node_path11.default.resolve(root);
3929
3953
  this.listTimeoutMs = options.listTimeoutMs ?? LIST_FILES_TIMEOUT_MS;
3930
3954
  this.trackedOnly = options.trackedOnly ?? false;
3931
3955
  }
@@ -3987,13 +4011,13 @@ var CheckoutFilePlane = class {
3987
4011
  if (!relative) continue;
3988
4012
  let stat2;
3989
4013
  try {
3990
- stat2 = import_node_fs10.default.statSync(import_node_path10.default.join(this.root, relative));
4014
+ stat2 = import_node_fs11.default.statSync(import_node_path11.default.join(this.root, relative));
3991
4015
  } catch {
3992
4016
  continue;
3993
4017
  }
3994
4018
  if (!stat2.isFile()) continue;
3995
4019
  const seen = this.sniffed.get(relative);
3996
- const binary = seen && seen.size === stat2.size && seen.mtimeMs === stat2.mtimeMs ? seen.binary : looksBinary(import_node_path10.default.join(this.root, relative), stat2.size);
4020
+ const binary = seen && seen.size === stat2.size && seen.mtimeMs === stat2.mtimeMs ? seen.binary : looksBinary(import_node_path11.default.join(this.root, relative), stat2.size);
3997
4021
  nextSniffed.set(relative, { size: stat2.size, mtimeMs: stat2.mtimeMs, binary });
3998
4022
  next.set(relative, {
3999
4023
  size: stat2.size,
@@ -4007,22 +4031,22 @@ var CheckoutFilePlane = class {
4007
4031
  this.sniffed = nextSniffed;
4008
4032
  }
4009
4033
  absolute(planePath) {
4010
- const resolved = import_node_path10.default.resolve(this.root, planePath);
4011
- if (resolved !== this.root && !resolved.startsWith(`${this.root}${import_node_path10.default.sep}`)) {
4034
+ const resolved = import_node_path11.default.resolve(this.root, planePath);
4035
+ if (resolved !== this.root && !resolved.startsWith(`${this.root}${import_node_path11.default.sep}`)) {
4012
4036
  throw new Error(`path escapes the project: ${planePath}`);
4013
4037
  }
4014
4038
  return resolved;
4015
4039
  }
4016
4040
  async read(planePath) {
4017
- return import_node_fs10.default.promises.readFile(this.absolute(planePath), "utf8");
4041
+ return import_node_fs11.default.promises.readFile(this.absolute(planePath), "utf8");
4018
4042
  }
4019
4043
  async readBytes(planePath) {
4020
- return import_node_fs10.default.promises.readFile(this.absolute(planePath));
4044
+ return import_node_fs11.default.promises.readFile(this.absolute(planePath));
4021
4045
  }
4022
4046
  async write(planePath, content) {
4023
4047
  const absolute = this.absolute(planePath);
4024
- await import_node_fs10.default.promises.mkdir(import_node_path10.default.dirname(absolute), { recursive: true });
4025
- await import_node_fs10.default.promises.writeFile(absolute, content);
4048
+ await import_node_fs11.default.promises.mkdir(import_node_path11.default.dirname(absolute), { recursive: true });
4049
+ await import_node_fs11.default.promises.writeFile(absolute, content);
4026
4050
  this.entries.set(planePath, {
4027
4051
  size: content.byteLength,
4028
4052
  readOnly: false,
@@ -4031,7 +4055,7 @@ var CheckoutFilePlane = class {
4031
4055
  });
4032
4056
  }
4033
4057
  async delete(planePath) {
4034
- await import_node_fs10.default.promises.rm(this.absolute(planePath), { force: true });
4058
+ await import_node_fs11.default.promises.rm(this.absolute(planePath), { force: true });
4035
4059
  this.entries.delete(planePath);
4036
4060
  }
4037
4061
  };
@@ -4157,14 +4181,14 @@ var PlaneHost = class {
4157
4181
  // root and an opened one cannot disagree about the same directory.
4158
4182
  realRoot(root) {
4159
4183
  if (!root) throw new Error("root is required");
4160
- const resolved = import_node_path11.default.resolve(root);
4184
+ const resolved = import_node_path12.default.resolve(root);
4161
4185
  let real;
4162
4186
  try {
4163
- real = import_node_fs11.default.realpathSync(resolved);
4187
+ real = import_node_fs12.default.realpathSync(resolved);
4164
4188
  } catch {
4165
4189
  throw new Error(`no such directory: ${resolved}`);
4166
4190
  }
4167
- if (!import_node_fs11.default.statSync(real).isDirectory()) throw new Error(`not a directory: ${real}`);
4191
+ if (!import_node_fs12.default.statSync(real).isDirectory()) throw new Error(`not a directory: ${real}`);
4168
4192
  return real;
4169
4193
  }
4170
4194
  respond(ws, id, result) {
@@ -4185,11 +4209,11 @@ var PlaneHost = class {
4185
4209
 
4186
4210
  // src/host/replacement-module-handler.ts
4187
4211
  var import_promises5 = __toESM(require("node:fs/promises"), 1);
4188
- var import_node_path12 = __toESM(require("node:path"), 1);
4212
+ var import_node_path13 = __toESM(require("node:path"), 1);
4189
4213
  var import_esbuild = require("esbuild");
4190
4214
  var REPLACEMENT_MODULE_ROUTE = "/__sootsim-replacement-module";
4191
4215
  function getReplacementLoader(filePath) {
4192
- const ext = import_node_path12.default.extname(filePath).toLowerCase();
4216
+ const ext = import_node_path13.default.extname(filePath).toLowerCase();
4193
4217
  switch (ext) {
4194
4218
  case ".js":
4195
4219
  return "js";
@@ -4271,7 +4295,7 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
4271
4295
  res.end("replacement files must be .js, .jsx, .ts, or .tsx");
4272
4296
  return true;
4273
4297
  }
4274
- if (!import_node_path12.default.isAbsolute(targetPath)) {
4298
+ if (!import_node_path13.default.isAbsolute(targetPath)) {
4275
4299
  res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
4276
4300
  res.end("replacement file path must be absolute");
4277
4301
  return true;
@@ -4279,10 +4303,10 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
4279
4303
  const runtimePackages = new Set(url.searchParams.getAll("runtimePackage"));
4280
4304
  let resolvedProjectRoot;
4281
4305
  if (options.projectRoot) {
4282
- const lexicalProjectRoot = import_node_path12.default.resolve(options.projectRoot);
4306
+ const lexicalProjectRoot = import_node_path13.default.resolve(options.projectRoot);
4283
4307
  resolvedProjectRoot = await import_promises5.default.realpath(options.projectRoot);
4284
- const lexicalTarget = import_node_path12.default.relative(lexicalProjectRoot, targetPath);
4285
- if (lexicalTarget.startsWith("..") || import_node_path12.default.isAbsolute(lexicalTarget)) {
4308
+ const lexicalTarget = import_node_path13.default.relative(lexicalProjectRoot, targetPath);
4309
+ if (lexicalTarget.startsWith("..") || import_node_path13.default.isAbsolute(lexicalTarget)) {
4286
4310
  res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
4287
4311
  res.end("replacement file must be inside the project root");
4288
4312
  return true;
@@ -4298,8 +4322,8 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
4298
4322
  return true;
4299
4323
  }
4300
4324
  if (resolvedProjectRoot) {
4301
- const relativeTarget = import_node_path12.default.relative(resolvedProjectRoot, resolvedTargetPath);
4302
- if (relativeTarget.startsWith("..") || import_node_path12.default.isAbsolute(relativeTarget)) {
4325
+ const relativeTarget = import_node_path13.default.relative(resolvedProjectRoot, resolvedTargetPath);
4326
+ if (relativeTarget.startsWith("..") || import_node_path13.default.isAbsolute(relativeTarget)) {
4303
4327
  res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
4304
4328
  res.end("replacement file must be inside the project root");
4305
4329
  return true;
@@ -4386,8 +4410,13 @@ exports.jsxDEV = exports.jsx`,
4386
4410
  }
4387
4411
 
4388
4412
  // src/host/websocket-proxy.ts
4389
- var import_ws = require("ws");
4413
+ var import_crypto = require("crypto");
4414
+ var import_http3 = require("http");
4415
+ var import_https2 = require("https");
4390
4416
  var WEBSOCKET_PROXY_PATH = "/__websocket-proxy";
4417
+ var WEBSOCKET_PROXY_HANDSHAKE_TIMEOUT_MS = 1e4;
4418
+ var WEBSOCKET_PROTOCOL_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
4419
+ var WEBSOCKET_KEY = /^[+/0-9A-Za-z]{22}==$/;
4391
4420
  var STRIP_UPSTREAM_HEADERS = /* @__PURE__ */ new Set([
4392
4421
  "host",
4393
4422
  "connection",
@@ -4400,20 +4429,62 @@ var STRIP_UPSTREAM_HEADERS = /* @__PURE__ */ new Set([
4400
4429
  "sec-websocket-protocol",
4401
4430
  "sec-websocket-version"
4402
4431
  ]);
4403
- function rejectUpgrade(socket, status, message) {
4432
+ function rejectUpgrade(socket, status, message, headers = {}) {
4404
4433
  try {
4434
+ const extraHeaders = Object.entries(headers).map(([name2, value]) => `${name2}: ${value}\r
4435
+ `).join("");
4405
4436
  socket.write(
4406
4437
  `HTTP/1.1 ${status} ${message}\r
4407
4438
  Connection: close\r
4408
4439
  Content-Type: text/plain\r
4409
4440
  Content-Length: ${message.length}\r
4410
- \r
4441
+ ${extraHeaders}\r
4411
4442
  ${message}`
4412
4443
  );
4413
4444
  } catch {
4414
4445
  }
4415
4446
  socket.destroy();
4416
4447
  }
4448
+ function hasHeaderToken(value, token) {
4449
+ return (value || "").split(",").some((part) => part.trim().toLowerCase() === token);
4450
+ }
4451
+ function hasValidProtocolOffer(value) {
4452
+ if (value === void 0) return true;
4453
+ if (!WEBSOCKET_PROTOCOL_TOKEN.test(value[0] || "")) return false;
4454
+ if (!WEBSOCKET_PROTOCOL_TOKEN.test(value[value.length - 1] || "")) return false;
4455
+ const protocols = /* @__PURE__ */ new Set();
4456
+ for (const part of value.split(",")) {
4457
+ const protocol = part.trim();
4458
+ if (!WEBSOCKET_PROTOCOL_TOKEN.test(protocol) || protocols.has(protocol)) {
4459
+ return false;
4460
+ }
4461
+ protocols.add(protocol);
4462
+ }
4463
+ return true;
4464
+ }
4465
+ function validateUpgradeRequest(req) {
4466
+ if (req.method !== "GET") {
4467
+ return { status: 405, message: "invalid websocket http method" };
4468
+ }
4469
+ if (!hasHeaderToken(req.headers.connection, "upgrade") || !hasHeaderToken(req.headers.upgrade, "websocket")) {
4470
+ return { status: 400, message: "invalid websocket upgrade headers" };
4471
+ }
4472
+ const key = req.headers["sec-websocket-key"];
4473
+ if (typeof key !== "string" || !WEBSOCKET_KEY.test(key) || Buffer.from(key, "base64").byteLength !== 16 || Buffer.from(key, "base64").toString("base64") !== key) {
4474
+ return { status: 400, message: "invalid websocket key" };
4475
+ }
4476
+ if (req.headers["sec-websocket-version"] !== "13") {
4477
+ return {
4478
+ status: 400,
4479
+ message: "invalid websocket version",
4480
+ headers: { "Sec-WebSocket-Version": "13" }
4481
+ };
4482
+ }
4483
+ if (!hasValidProtocolOffer(req.headers["sec-websocket-protocol"])) {
4484
+ return { status: 400, message: "invalid websocket protocol offer" };
4485
+ }
4486
+ return null;
4487
+ }
4417
4488
  function isSameOriginUpgrade(req) {
4418
4489
  const origin = req.headers.origin;
4419
4490
  const host = req.headers.host;
@@ -4443,61 +4514,29 @@ function getDefaultWebSocketOrigin(targetUrl) {
4443
4514
  origin.protocol = targetUrl.protocol === "wss:" ? "https:" : "http:";
4444
4515
  return origin.origin;
4445
4516
  }
4446
- function getRequestedProtocols(req) {
4447
- const header = req.headers["sec-websocket-protocol"];
4448
- const value = Array.isArray(header) ? header.join(",") : header || "";
4449
- return value.split(",").map((part) => part.trim()).filter(Boolean);
4450
- }
4451
- function safeClose(ws, code, reason) {
4452
- if (ws.readyState === import_ws.WebSocket.CLOSED || ws.readyState === import_ws.WebSocket.CLOSING) {
4453
- return;
4454
- }
4455
- try {
4456
- ws.close(code, reason);
4457
- } catch {
4458
- ws.terminate();
4459
- }
4460
- }
4461
- function connectProxyPair(clientWs, upstream) {
4462
- let closing = false;
4463
- const closeBoth = (source, target, code, reason) => {
4464
- if (closing) return;
4465
- closing = true;
4466
- safeClose(target, code, reason.toString());
4467
- if (source.readyState === import_ws.WebSocket.OPEN) {
4468
- safeClose(source, code, reason.toString());
4469
- }
4470
- };
4471
- clientWs.on("message", (data, isBinary) => {
4472
- if (upstream.readyState === import_ws.WebSocket.OPEN) {
4473
- upstream.send(data, { binary: isBinary });
4474
- }
4475
- });
4476
- upstream.on("message", (data, isBinary) => {
4477
- if (clientWs.readyState === import_ws.WebSocket.OPEN) {
4478
- clientWs.send(data, { binary: isBinary });
4479
- }
4480
- });
4481
- clientWs.on("close", (code, reason) => closeBoth(clientWs, upstream, code, reason));
4482
- upstream.on("close", (code, reason) => closeBoth(upstream, clientWs, code, reason));
4483
- clientWs.on("error", () => safeClose(upstream, 1011, "proxy client error"));
4484
- upstream.on("error", () => safeClose(clientWs, 1011, "upstream websocket error"));
4485
- }
4486
- function createUpstreamWebSocket(targetUrl, protocols, headers, forwardOrigin) {
4517
+ function createUpstreamHeaders(req, targetUrl, headers, forwardOrigin) {
4487
4518
  const upstreamHeaders = { ...headers };
4488
4519
  const originKeys = Object.keys(upstreamHeaders).filter(
4489
- (key) => key.toLowerCase() === "origin"
4520
+ (key2) => key2.toLowerCase() === "origin"
4490
4521
  );
4491
4522
  if (forwardOrigin && originKeys.length === 0) {
4492
4523
  upstreamHeaders.origin = getDefaultWebSocketOrigin(targetUrl);
4493
4524
  } else if (!forwardOrigin) {
4494
- for (const key of originKeys) delete upstreamHeaders[key];
4495
- }
4496
- if (Object.keys(upstreamHeaders).length === 0)
4497
- return new import_ws.WebSocket(targetUrl.href, protocols);
4498
- return new import_ws.WebSocket(targetUrl.href, protocols, {
4499
- headers: upstreamHeaders
4500
- });
4525
+ for (const key2 of originKeys) delete upstreamHeaders[key2];
4526
+ }
4527
+ upstreamHeaders.connection = "Upgrade";
4528
+ upstreamHeaders.upgrade = "websocket";
4529
+ const key = req.headers["sec-websocket-key"];
4530
+ if (typeof key === "string") upstreamHeaders["sec-websocket-key"] = key;
4531
+ const version = req.headers["sec-websocket-version"];
4532
+ if (typeof version === "string") upstreamHeaders["sec-websocket-version"] = version;
4533
+ const protocol = req.headers["sec-websocket-protocol"];
4534
+ if (typeof protocol === "string") upstreamHeaders["sec-websocket-protocol"] = protocol;
4535
+ const extensions = req.headers["sec-websocket-extensions"];
4536
+ if (typeof extensions === "string") {
4537
+ upstreamHeaders["sec-websocket-extensions"] = extensions;
4538
+ }
4539
+ return upstreamHeaders;
4501
4540
  }
4502
4541
  function isWebSocketProxyRequestUrl(rawUrl) {
4503
4542
  if (!rawUrl) return false;
@@ -4513,6 +4552,16 @@ function handleWebSocketProxyUpgrade(req, socket, head, forwardOrigin = true) {
4513
4552
  rejectUpgrade(socket, 403, "forbidden websocket proxy origin");
4514
4553
  return true;
4515
4554
  }
4555
+ const upgradeRejection = validateUpgradeRequest(req);
4556
+ if (upgradeRejection) {
4557
+ rejectUpgrade(
4558
+ socket,
4559
+ upgradeRejection.status,
4560
+ upgradeRejection.message,
4561
+ upgradeRejection.headers
4562
+ );
4563
+ return true;
4564
+ }
4516
4565
  let targetUrl;
4517
4566
  let headers;
4518
4567
  try {
@@ -4532,37 +4581,94 @@ function handleWebSocketProxyUpgrade(req, socket, head, forwardOrigin = true) {
4532
4581
  rejectUpgrade(socket, 400, "invalid websocket proxy request");
4533
4582
  return true;
4534
4583
  }
4535
- const protocols = getRequestedProtocols(req);
4536
- const upstream = createUpstreamWebSocket(targetUrl, protocols, headers, forwardOrigin);
4537
- let completed = false;
4584
+ const upstreamHeaders = createUpstreamHeaders(req, targetUrl, headers, forwardOrigin);
4585
+ let settled = false;
4586
+ const request = targetUrl.protocol === "wss:" ? import_https2.request : import_http3.request;
4587
+ const upstreamRequestUrl = new URL(targetUrl);
4588
+ upstreamRequestUrl.protocol = targetUrl.protocol === "wss:" ? "https:" : "http:";
4589
+ const upstreamRequest = request(upstreamRequestUrl, {
4590
+ method: "GET",
4591
+ headers: upstreamHeaders
4592
+ });
4593
+ let destroyUpstreamSocket = () => {
4594
+ };
4595
+ upstreamRequest.once("socket", (upstreamSocket) => {
4596
+ destroyUpstreamSocket = () => upstreamSocket.destroy();
4597
+ });
4598
+ const fail = (status, message) => {
4599
+ if (settled) return;
4600
+ settled = true;
4601
+ clearTimeout(timer);
4602
+ upstreamRequest.destroy();
4603
+ destroyUpstreamSocket();
4604
+ rejectUpgrade(socket, status, message);
4605
+ };
4606
+ const timer = setTimeout(() => {
4607
+ fail(504, "upstream websocket handshake timeout");
4608
+ }, WEBSOCKET_PROXY_HANDSHAKE_TIMEOUT_MS);
4538
4609
  socket.once("close", () => {
4539
- if (!completed) upstream.terminate();
4610
+ if (!settled) {
4611
+ settled = true;
4612
+ clearTimeout(timer);
4613
+ }
4614
+ upstreamRequest.destroy();
4615
+ destroyUpstreamSocket();
4540
4616
  });
4541
- upstream.once("open", () => {
4542
- if (completed) return;
4543
- completed = true;
4544
- const selectedProtocol = upstream.protocol;
4545
- const proxyServer = new import_ws.WebSocketServer({
4546
- noServer: true,
4547
- clientTracking: false,
4548
- handleProtocols(requestedProtocols) {
4549
- return selectedProtocol || requestedProtocols.values().next().value || false;
4550
- }
4551
- });
4552
- proxyServer.handleUpgrade(req, socket, head, (clientWs) => {
4553
- connectProxyPair(clientWs, upstream);
4554
- });
4617
+ upstreamRequest.once("response", (response) => {
4618
+ response.resume();
4619
+ fail(502, "upstream websocket rejected upgrade");
4555
4620
  });
4556
- upstream.once("error", () => {
4557
- if (completed) return;
4558
- completed = true;
4559
- rejectUpgrade(socket, 502, "upstream websocket error");
4621
+ upstreamRequest.once("error", () => {
4622
+ fail(502, "upstream websocket error");
4560
4623
  });
4561
- upstream.once("close", () => {
4562
- if (completed) return;
4563
- completed = true;
4564
- rejectUpgrade(socket, 502, "upstream websocket closed");
4624
+ upstreamRequest.once("upgrade", (response, upstreamSocket, upstreamHead) => {
4625
+ if (settled) {
4626
+ upstreamSocket.destroy();
4627
+ return;
4628
+ }
4629
+ const accept = response.headers["sec-websocket-accept"];
4630
+ const upgrade = response.headers.upgrade;
4631
+ const requestKey = req.headers["sec-websocket-key"];
4632
+ const expectedAccept = typeof requestKey === "string" ? (0, import_crypto.createHash)("sha1").update(`${requestKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64") : "";
4633
+ if (typeof accept !== "string" || accept !== expectedAccept || typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket") {
4634
+ upstreamSocket.destroy();
4635
+ fail(502, "upstream websocket handshake is invalid");
4636
+ return;
4637
+ }
4638
+ const selectedProtocol = response.headers["sec-websocket-protocol"];
4639
+ const requestedProtocols = (req.headers["sec-websocket-protocol"] || "").split(",").map((protocol) => protocol.trim());
4640
+ if (typeof selectedProtocol === "string" && !requestedProtocols.includes(selectedProtocol)) {
4641
+ upstreamSocket.destroy();
4642
+ fail(502, "upstream websocket selected an unrequested protocol");
4643
+ return;
4644
+ }
4645
+ settled = true;
4646
+ clearTimeout(timer);
4647
+ const responseHeaders = [
4648
+ "HTTP/1.1 101 Switching Protocols",
4649
+ "Upgrade: websocket",
4650
+ "Connection: Upgrade",
4651
+ `Sec-WebSocket-Accept: ${accept}`
4652
+ ];
4653
+ if (typeof selectedProtocol === "string") {
4654
+ responseHeaders.push(`Sec-WebSocket-Protocol: ${selectedProtocol}`);
4655
+ }
4656
+ const selectedExtensions = response.headers["sec-websocket-extensions"];
4657
+ if (typeof selectedExtensions === "string") {
4658
+ responseHeaders.push(`Sec-WebSocket-Extensions: ${selectedExtensions}`);
4659
+ }
4660
+ socket.write(`${responseHeaders.join("\r\n")}\r
4661
+ \r
4662
+ `);
4663
+ if (head.length > 0) upstreamSocket.write(head);
4664
+ if (upstreamHead.length > 0) socket.write(upstreamHead);
4665
+ socket.once("close", () => upstreamSocket.destroy());
4666
+ socket.once("error", () => upstreamSocket.destroy());
4667
+ upstreamSocket.once("error", () => socket.destroy());
4668
+ socket.pipe(upstreamSocket);
4669
+ upstreamSocket.pipe(socket);
4565
4670
  });
4671
+ upstreamRequest.end();
4566
4672
  return true;
4567
4673
  }
4568
4674
 
@@ -4807,7 +4913,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
4807
4913
  }
4808
4914
  bindOnce(port, _silent) {
4809
4915
  return new Promise((resolve2, reject) => {
4810
- const server = (0, import_http3.createServer)((req, res) => this.handleHttpRequest(req, res));
4916
+ const server = (0, import_http4.createServer)((req, res) => this.handleHttpRequest(req, res));
4811
4917
  let settled = false;
4812
4918
  const onError = (err) => {
4813
4919
  if (settled) return;
@@ -4830,7 +4936,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
4830
4936
  `);
4831
4937
  });
4832
4938
  this.httpServer = server;
4833
- this.wss = new import_ws2.WebSocketServer({ noServer: true });
4939
+ this.wss = new import_ws.WebSocketServer({ noServer: true });
4834
4940
  this.wireWebSocketServer();
4835
4941
  server.on("upgrade", (req, socket, head) => {
4836
4942
  if (handleWebSocketProxyUpgrade(req, socket, head)) return;
@@ -5068,7 +5174,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5068
5174
  const entry = this.cliBySentId.get(msg.id);
5069
5175
  if (entry) {
5070
5176
  this.cliBySentId.delete(msg.id);
5071
- if (entry.ws.readyState === import_ws2.WebSocket.OPEN) {
5177
+ if (entry.ws.readyState === import_ws.WebSocket.OPEN) {
5072
5178
  const otherCliCount = this.getOtherCliIdentityCount(entry.ws, entry.simId);
5073
5179
  const response = otherCliCount > 0 ? { ...msg, id: entry.originalId, _otherCliCount: otherCliCount } : { ...msg, id: entry.originalId };
5074
5180
  entry.ws.send(JSON.stringify(response));
@@ -5096,7 +5202,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5096
5202
  if (typeof msg.cliLabel === "string" && msg.cliLabel.trim()) {
5097
5203
  this.cliLabelBySocket.set(ws, msg.cliLabel.trim());
5098
5204
  }
5099
- if (ws.readyState === import_ws2.WebSocket.OPEN) {
5205
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
5100
5206
  ws.send(
5101
5207
  JSON.stringify({
5102
5208
  id: msg.id,
@@ -5111,7 +5217,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5111
5217
  return;
5112
5218
  }
5113
5219
  if (msg.type === "bridge:list-sims") {
5114
- if (ws.readyState === import_ws2.WebSocket.OPEN) {
5220
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
5115
5221
  ws.send(
5116
5222
  JSON.stringify({
5117
5223
  id: msg.id,
@@ -5127,7 +5233,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5127
5233
  force: msg.force === true
5128
5234
  });
5129
5235
  if (!outcome.granted) {
5130
- if (ws.readyState === import_ws2.WebSocket.OPEN) {
5236
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
5131
5237
  ws.send(
5132
5238
  JSON.stringify({
5133
5239
  id: msg.id,
@@ -5143,7 +5249,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5143
5249
  targetSim2.id,
5144
5250
  outcome.bootedCount > 0 ? `cli force-claimed sim (booted ${outcome.bootedCount})` : "cli claimed sim"
5145
5251
  );
5146
- if (ws.readyState === import_ws2.WebSocket.OPEN) {
5252
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
5147
5253
  ws.send(
5148
5254
  JSON.stringify({
5149
5255
  id: msg.id,
@@ -5162,7 +5268,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5162
5268
  if (shouldAcquireLease(msg)) {
5163
5269
  const outcome = this.tryAcquireLease(ws, targetSim);
5164
5270
  if (!outcome.granted) {
5165
- if (ws.readyState === import_ws2.WebSocket.OPEN) {
5271
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
5166
5272
  ws.send(
5167
5273
  JSON.stringify({
5168
5274
  id: msg.id,
@@ -5188,7 +5294,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5188
5294
  targetSim.ws.send(JSON.stringify({ ...forwarded, id: sentId }));
5189
5295
  if (forwarded.type === "close") {
5190
5296
  this.cliBySentId.delete(sentId);
5191
- if (ws.readyState === import_ws2.WebSocket.OPEN) {
5297
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
5192
5298
  ws.send(
5193
5299
  JSON.stringify({
5194
5300
  id: msg.id,
@@ -5203,7 +5309,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5203
5309
  unrefTimer(closeTimer);
5204
5310
  }
5205
5311
  } catch (err) {
5206
- if (ws.readyState === import_ws2.WebSocket.OPEN) {
5312
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
5207
5313
  ws.send(
5208
5314
  JSON.stringify({
5209
5315
  id: msg.id,
@@ -5229,7 +5335,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5229
5335
  }
5230
5336
  for (const [sentId, entry] of this.cliBySentId) {
5231
5337
  if (entry.simId !== sim.id) continue;
5232
- if (entry.ws.readyState === import_ws2.WebSocket.OPEN) {
5338
+ if (entry.ws.readyState === import_ws.WebSocket.OPEN) {
5233
5339
  entry.ws.send(
5234
5340
  JSON.stringify({
5235
5341
  id: entry.originalId,
@@ -5601,7 +5707,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5601
5707
  if (runtimeVersionFromHostname(simUrl.hostname)) continue;
5602
5708
  } catch {
5603
5709
  }
5604
- if (sim.ws.readyState === import_ws2.WebSocket.OPEN) {
5710
+ if (sim.ws.readyState === import_ws.WebSocket.OPEN) {
5605
5711
  try {
5606
5712
  sim.ws.send(payload);
5607
5713
  } catch {
@@ -5985,7 +6091,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
5985
6091
  sweepDeadWebSockets() {
5986
6092
  if (!this.wss) return;
5987
6093
  for (const ws of this.wss.clients) {
5988
- if (ws.readyState !== import_ws2.WebSocket.OPEN) continue;
6094
+ if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
5989
6095
  const alive = this.wsIsAlive.get(ws);
5990
6096
  if (alive === false) {
5991
6097
  try {
@@ -6006,7 +6112,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6006
6112
  }
6007
6113
  }
6008
6114
  closeSimSocketFromHost(ws) {
6009
- if (ws.readyState !== import_ws2.WebSocket.OPEN) return;
6115
+ if (ws.readyState !== import_ws.WebSocket.OPEN) return;
6010
6116
  try {
6011
6117
  ws.close(SOOTSIM_BRIDGE_SIM_CLOSE_CODE, SOOTSIM_BRIDGE_SIM_CLOSE_REASON);
6012
6118
  } catch {
@@ -6017,7 +6123,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6017
6123
  return;
6018
6124
  }
6019
6125
  const terminateTimer = setTimeout(() => {
6020
- if (ws.readyState === import_ws2.WebSocket.CLOSED) return;
6126
+ if (ws.readyState === import_ws.WebSocket.CLOSED) return;
6021
6127
  try {
6022
6128
  ws.terminate();
6023
6129
  } catch {
@@ -6180,7 +6286,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6180
6286
  try {
6181
6287
  readyState = sim.ws.readyState;
6182
6288
  } catch {
6183
- readyState = import_ws2.WebSocket.CLOSED;
6289
+ readyState = import_ws.WebSocket.CLOSED;
6184
6290
  }
6185
6291
  const lease = this.getActiveLease(sim);
6186
6292
  return {
@@ -6193,7 +6299,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6193
6299
  lastSeenAt: sim.lastSeenAt,
6194
6300
  lastActiveAt: sim.lastActiveAt || void 0,
6195
6301
  isPrimary: sim.id === this.primarySimId,
6196
- readyState: readyState === import_ws2.WebSocket.OPEN ? "open" : readyState === import_ws2.WebSocket.CLOSING ? "closing" : "closed",
6302
+ readyState: readyState === import_ws.WebSocket.OPEN ? "open" : readyState === import_ws.WebSocket.CLOSING ? "closing" : "closed",
6197
6303
  attachedCliCount: this.getAttachedCliCount(sim.id),
6198
6304
  lockedBy: lease ? lease.cliLabel || lease.cliIdentityKey : void 0,
6199
6305
  lockedByKind: lease ? lease.kind : void 0,
@@ -6310,7 +6416,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6310
6416
  getOpenSim(simId) {
6311
6417
  if (simId) {
6312
6418
  const sim = this.sims.get(simId);
6313
- if (sim?.ws.readyState === import_ws2.WebSocket.OPEN) return sim;
6419
+ if (sim?.ws.readyState === import_ws.WebSocket.OPEN) return sim;
6314
6420
  return null;
6315
6421
  }
6316
6422
  const candidates = this.getDefaultSimCandidates();
@@ -6318,7 +6424,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6318
6424
  }
6319
6425
  getDefaultSimCandidates() {
6320
6426
  const open = Array.from(this.sims.values()).filter(
6321
- (sim) => sim.ws.readyState === import_ws2.WebSocket.OPEN
6427
+ (sim) => sim.ws.readyState === import_ws.WebSocket.OPEN
6322
6428
  );
6323
6429
  const registered = open.filter((sim) => sim.url);
6324
6430
  return registered.length > 0 ? registered : open;
@@ -6351,7 +6457,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6351
6457
  shouldPromoteSim(sim) {
6352
6458
  const current = this.primarySimId ? this.sims.get(this.primarySimId) : null;
6353
6459
  if (!sim.url) return !current;
6354
- const currentAlive = current?.ws.readyState === import_ws2.WebSocket.OPEN;
6460
+ const currentAlive = current?.ws.readyState === import_ws.WebSocket.OPEN;
6355
6461
  if (!current || !currentAlive || !current.url) return true;
6356
6462
  const isPrimaryCandidate = sim.origin?.includes(":5173");
6357
6463
  const currentIsPrimary = current.origin?.includes(":5173");
@@ -6359,7 +6465,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6359
6465
  }
6360
6466
  broadcastSimAssignments() {
6361
6467
  for (const sim of this.sims.values()) {
6362
- if (sim.ws.readyState !== import_ws2.WebSocket.OPEN) continue;
6468
+ if (sim.ws.readyState !== import_ws.WebSocket.OPEN) continue;
6363
6469
  sim.ws.send(
6364
6470
  JSON.stringify({
6365
6471
  type: "bridge:welcome",
@@ -6376,7 +6482,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6376
6482
  config: next
6377
6483
  });
6378
6484
  for (const peer of this.sims.values()) {
6379
- if (peer.ws.readyState !== import_ws2.WebSocket.OPEN) continue;
6485
+ if (peer.ws.readyState !== import_ws.WebSocket.OPEN) continue;
6380
6486
  try {
6381
6487
  peer.ws.send(payload);
6382
6488
  } catch {
@@ -6386,7 +6492,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6386
6492
  }
6387
6493
  broadcastSimClientStates() {
6388
6494
  for (const sim of this.sims.values()) {
6389
- if (sim.ws.readyState !== import_ws2.WebSocket.OPEN) continue;
6495
+ if (sim.ws.readyState !== import_ws.WebSocket.OPEN) continue;
6390
6496
  const lease = this.getActiveLease(sim);
6391
6497
  const message = {
6392
6498
  type: "bridge:client-state",
@@ -6457,7 +6563,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6457
6563
  const keys = /* @__PURE__ */ new Set();
6458
6564
  for (const [ws, attachedSimId] of this.cliSimBySocket) {
6459
6565
  if (attachedSimId !== simId) continue;
6460
- if (ws.readyState !== import_ws2.WebSocket.OPEN) continue;
6566
+ if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
6461
6567
  const key = this.cliIdentityKeyBySocket.get(ws);
6462
6568
  keys.add(key ?? `ws-unknown-${keys.size}`);
6463
6569
  }
@@ -6470,7 +6576,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6470
6576
  const keys = /* @__PURE__ */ new Set();
6471
6577
  for (const [ws, attachedSimId] of this.cliSimBySocket) {
6472
6578
  if (attachedSimId !== simId) continue;
6473
- if (ws.readyState !== import_ws2.WebSocket.OPEN) continue;
6579
+ if (ws.readyState !== import_ws.WebSocket.OPEN) continue;
6474
6580
  const key = this.cliIdentityKeyBySocket.get(ws);
6475
6581
  if (key && key === selfKey) continue;
6476
6582
  keys.add(key ?? `ws-unknown-${keys.size}`);
@@ -6495,7 +6601,7 @@ var SootSimBridgeHost = class _SootSimBridgeHost {
6495
6601
  const nextId = requestedId?.trim();
6496
6602
  if (!nextId || nextId === sim.id) return false;
6497
6603
  const existing = this.sims.get(nextId);
6498
- if (existing && existing !== sim && existing.ws.readyState === import_ws2.WebSocket.OPEN) {
6604
+ if (existing && existing !== sim && existing.ws.readyState === import_ws.WebSocket.OPEN) {
6499
6605
  return false;
6500
6606
  }
6501
6607
  const restorable = this.getRestorableSimState(nextId);