@volter/world-runtime 3.0.128 → 3.0.130

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.
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
3
4
 
4
5
  // Retain the existing 1000 ms inspection ceiling, not a new timeout chosen from one sample.
5
6
  // Measured 2026-10-03 on shared Darwin 25.4.0 ARM64, Bun 1.3.11 (process.version
@@ -8,10 +9,33 @@ import { readFileSync } from 'node:fs';
8
9
  // TZ=UTC took 9.26 ms from spawn to close; time printed real/user/sys 0.00 s.
9
10
  // This includes the time wrapper/event-loop overhead, not an isolated benchmark.
10
11
  // Full command/output: cookbook/calcom/output/held-backing-lifecycle.txt.
12
+ // Windows, measured 2026-10-07 on Windows 11 Home 10.0.26200 x64 (volter-desktop, a livestream
13
+ // running, CPU load 30%), Node v24.18.0, parent e8d4b55f plus this change: `await processBirth(process.pid)`
14
+ // from this module, timed with performance.now() around the await, took 237-319 ms over 10
15
+ // sequential runs, at most 298 ms with 4 awaited at once and at most 424 ms with 8.
11
16
  const BIRTH_LOOKUP_MS = 1000;
17
+ /** The command that prints a pid's birth, and how its output reads. Windows has no `ps` and no /proc: its birth is the
18
+ * process's creation time (GetProcessTimes, a FILETIME of 100 ns ticks in UTC, so neither the caller's zone nor its
19
+ * locale changes it), read through .NET by the Windows PowerShell every Windows 10 and 11 install ships. */
20
+ function birthProbe(pid) {
21
+ if (process.platform === 'win32') {
22
+ return {
23
+ command: join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
24
+ args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', `[Diagnostics.Process]::GetProcessById(${pid}).StartTime.ToFileTimeUtc()`],
25
+ read: output => (/^\d+$/.test(output) ? `filetime ${output}` : undefined),
26
+ };
27
+ }
28
+ return {
29
+ command: 'ps', args: ['-o', 'pid=,lstart=', '-p', String(pid)],
30
+ read: output => {
31
+ const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(output);
32
+ return match && Number(match[1]) === pid ? match[2] : undefined;
33
+ },
34
+ };
35
+ }
12
36
  /** No subprocess on Linux; elsewhere the lookup never blocks the lifecycle event loop. */
13
37
  export async function processBirth(pid) {
14
- if (process.platform === 'win32') return undefined;
38
+ if (!Number.isInteger(pid) || pid <= 0) return undefined; // a pid enters a probe's command only as an integer
15
39
  if (process.platform === 'linux') {
16
40
  try {
17
41
  const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
@@ -19,13 +43,14 @@ export async function processBirth(pid) {
19
43
  return /^\d+$/.test(tick ?? '') ? `tick ${tick}` : undefined;
20
44
  } catch { return undefined; }
21
45
  }
46
+ const probe = birthProbe(pid);
22
47
  return new Promise(resolve => {
23
48
  // A probe that cannot run is a birth unknown, never a lifecycle failure: spawn throws synchronously where the
24
49
  // process may not exec `ps` at all (setuid root on macOS, which a sandbox-exec profile refuses: EPERM), and reports
25
50
  // other failures as 'error' (below). Either way the identity is the pid without a start time.
26
51
  let child;
27
52
  try {
28
- child = spawn('ps', ['-o', 'pid=,lstart=', '-p', String(pid)], {
53
+ child = spawn(probe.command, probe.args, {
29
54
  windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'],
30
55
  env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' },
31
56
  });
@@ -42,10 +67,7 @@ export async function processBirth(pid) {
42
67
  }, BIRTH_LOOKUP_MS);
43
68
  child.stdout.on('data', chunk => { output += chunk; });
44
69
  child.once('error', () => finish(undefined));
45
- child.once('close', code => {
46
- const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(output.trim());
47
- finish(code === 0 && match && Number(match[1]) === pid ? match[2] : undefined);
48
- });
70
+ child.once('close', code => finish(code === 0 ? probe.read(output.trim()) : undefined));
49
71
  });
50
72
  }
51
73
  /** Capture at spawn for durable ownership, never when retrying a retained PID. */
@@ -288,6 +288,24 @@ export function twinUnreachableMessage(vendor, origin, err, world) {
288
288
  const cause = err.cause;
289
289
  return `twin "${vendor}" at ${origin} is unreachable${world ? ` in World "${world}"` : ''}: ${errorMessage(err)}${cause?.code ? ` (${cause.code})` : cause?.message ? ` (${errorMessage(cause)})` : ''}; check \`volter-world doctor${world ? ` ${world}` : ''}\`\n`;
290
290
  }
291
+ /** The headers of one connection, not of the request (RFC 9110 §7.6.1), with `expect`: the hop that received them
292
+ * answered them (Node's server sends the 100 Continue itself). Node's fetch refuses `transfer-encoding`,
293
+ * `keep-alive`, `upgrade`, `expect` and a `connection` other than close/keep-alive outright, so a `git push` whose
294
+ * pack came chunked (larger than `http.postBuffer`) failed `fetch failed (UND_ERR_INVALID_ARG)`; the body streams
295
+ * on and fetch frames it afresh. */
296
+ const HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'te', 'trailer', 'upgrade', 'expect'];
297
+ const TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
298
+ function forwardableHeaders(incoming) {
299
+ const headers = new Headers(incoming);
300
+ for (const listed of (incoming.get('connection') ?? '').split(',')) {
301
+ const name = listed.trim();
302
+ if (TOKEN.test(name))
303
+ headers.delete(name);
304
+ }
305
+ for (const h of HOP_BY_HOP)
306
+ headers.delete(h);
307
+ return headers;
308
+ }
291
309
  /** Start the ambient redirect proxy. Returns a handle with the listen port + the env to export.
292
310
  * - Plain `http://` proxied requests to a vendor host → forwarded to the twin.
293
311
  * - `CONNECT host:443` to a vendor host → MITM-terminated with a session-signed leaf, decrypted, and
@@ -489,8 +507,8 @@ export async function startRedirectProxy(options) {
489
507
  return new Response(note(noTwinMessage(hostHeader, u.pathname)) + refusalText(gap).trimStart(), { status: 502, headers: { 'content-type': 'text/plain; charset=utf-8', [REFUSAL_HEADER]: gap.missing } });
490
508
  }
491
509
  const origin = new URL(twin.origin);
492
- const headers = new Headers(req.headers);
493
- for (const h of ['proxy-connection', 'accept-encoding', 'connection', 'x-volter-world-browser'])
510
+ const headers = forwardableHeaders(req.headers);
511
+ for (const h of ['accept-encoding', 'x-volter-world-browser'])
494
512
  headers.delete(h);
495
513
  // the application is not a twin: it gets no World key, keeps its Host and hears it was reached over TLS
496
514
  if (twin.vendor === 'app') {
@@ -521,7 +539,7 @@ export async function startRedirectProxy(options) {
521
539
  return new Response(upRes.body, { status: upRes.status, headers: out });
522
540
  }
523
541
  catch (err) {
524
- return new Response(twinUnreachable(twin.vendor, origin.origin, err), { status: 502 });
542
+ return new Response(twinUnreachable(twin.vendor, origin.origin, err), { status: 502, headers: { 'content-type': 'text/plain; charset=utf-8' } });
525
543
  }
526
544
  },
527
545
  upgrade: websocketRelay(req => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.128",
3
+ "version": "3.0.130",
4
4
  "description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
5
5
  "keywords": [
6
6
  "twin",
@@ -70,14 +70,14 @@
70
70
  "node": ">=22.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@volter/world-core": "3.0.128",
74
- "@volter/world-console": "3.0.123"
73
+ "@volter/world-core": "3.0.130",
74
+ "@volter/world-console": "3.0.125"
75
75
  },
76
76
  "dependencies": {
77
77
  "@electric-sql/pglite": "0.5.8",
78
78
  "@electric-sql/pglite-pgvector": "0.0.9",
79
79
  "@volter/world-access": "3.0.1",
80
- "@volter/world-core": "3.0.128",
80
+ "@volter/world-core": "3.0.130",
81
81
  "mongodb": "~6.20.0",
82
82
  "pg-gateway": "0.3.0-beta.4",
83
83
  "smol-toml": "^1.8.0",
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
3
4
 
4
5
  // Retain the existing 1000 ms inspection ceiling, not a new timeout chosen from one sample.
5
6
  // Measured 2026-10-03 on shared Darwin 25.4.0 ARM64, Bun 1.3.11 (process.version
@@ -8,10 +9,33 @@ import { readFileSync } from 'node:fs';
8
9
  // TZ=UTC took 9.26 ms from spawn to close; time printed real/user/sys 0.00 s.
9
10
  // This includes the time wrapper/event-loop overhead, not an isolated benchmark.
10
11
  // Full command/output: cookbook/calcom/output/held-backing-lifecycle.txt.
12
+ // Windows, measured 2026-10-07 on Windows 11 Home 10.0.26200 x64 (volter-desktop, a livestream
13
+ // running, CPU load 30%), Node v24.18.0, parent e8d4b55f plus this change: `await processBirth(process.pid)`
14
+ // from this module, timed with performance.now() around the await, took 237-319 ms over 10
15
+ // sequential runs, at most 298 ms with 4 awaited at once and at most 424 ms with 8.
11
16
  const BIRTH_LOOKUP_MS = 1000;
17
+ /** The command that prints a pid's birth, and how its output reads. Windows has no `ps` and no /proc: its birth is the
18
+ * process's creation time (GetProcessTimes, a FILETIME of 100 ns ticks in UTC, so neither the caller's zone nor its
19
+ * locale changes it), read through .NET by the Windows PowerShell every Windows 10 and 11 install ships. */
20
+ function birthProbe(pid) {
21
+ if (process.platform === 'win32') {
22
+ return {
23
+ command: join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
24
+ args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', `[Diagnostics.Process]::GetProcessById(${pid}).StartTime.ToFileTimeUtc()`],
25
+ read: output => (/^\d+$/.test(output) ? `filetime ${output}` : undefined),
26
+ };
27
+ }
28
+ return {
29
+ command: 'ps', args: ['-o', 'pid=,lstart=', '-p', String(pid)],
30
+ read: output => {
31
+ const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(output);
32
+ return match && Number(match[1]) === pid ? match[2] : undefined;
33
+ },
34
+ };
35
+ }
12
36
  /** No subprocess on Linux; elsewhere the lookup never blocks the lifecycle event loop. */
13
37
  export async function processBirth(pid) {
14
- if (process.platform === 'win32') return undefined;
38
+ if (!Number.isInteger(pid) || pid <= 0) return undefined; // a pid enters a probe's command only as an integer
15
39
  if (process.platform === 'linux') {
16
40
  try {
17
41
  const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
@@ -19,13 +43,14 @@ export async function processBirth(pid) {
19
43
  return /^\d+$/.test(tick ?? '') ? `tick ${tick}` : undefined;
20
44
  } catch { return undefined; }
21
45
  }
46
+ const probe = birthProbe(pid);
22
47
  return new Promise(resolve => {
23
48
  // A probe that cannot run is a birth unknown, never a lifecycle failure: spawn throws synchronously where the
24
49
  // process may not exec `ps` at all (setuid root on macOS, which a sandbox-exec profile refuses: EPERM), and reports
25
50
  // other failures as 'error' (below). Either way the identity is the pid without a start time.
26
51
  let child;
27
52
  try {
28
- child = spawn('ps', ['-o', 'pid=,lstart=', '-p', String(pid)], {
53
+ child = spawn(probe.command, probe.args, {
29
54
  windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'],
30
55
  env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' },
31
56
  });
@@ -42,10 +67,7 @@ export async function processBirth(pid) {
42
67
  }, BIRTH_LOOKUP_MS);
43
68
  child.stdout.on('data', chunk => { output += chunk; });
44
69
  child.once('error', () => finish(undefined));
45
- child.once('close', code => {
46
- const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(output.trim());
47
- finish(code === 0 && match && Number(match[1]) === pid ? match[2] : undefined);
48
- });
70
+ child.once('close', code => finish(code === 0 ? probe.read(output.trim()) : undefined));
49
71
  });
50
72
  }
51
73
  /** Capture at spawn for durable ownership, never when retrying a retained PID. */
@@ -336,6 +336,20 @@ export function twinUnreachableMessage(vendor: string, origin: string, err: Erro
336
336
  return `twin "${vendor}" at ${origin} is unreachable${world ? ` in World "${world}"` : ''}: ${errorMessage(err)}${cause?.code ? ` (${cause.code})` : cause?.message ? ` (${errorMessage(cause)})` : ''}; check \`volter-world doctor${world ? ` ${world}` : ''}\`\n`;
337
337
  }
338
338
 
339
+ /** The headers of one connection, not of the request (RFC 9110 §7.6.1), with `expect`: the hop that received them
340
+ * answered them (Node's server sends the 100 Continue itself). Node's fetch refuses `transfer-encoding`,
341
+ * `keep-alive`, `upgrade`, `expect` and a `connection` other than close/keep-alive outright, so a `git push` whose
342
+ * pack came chunked (larger than `http.postBuffer`) failed `fetch failed (UND_ERR_INVALID_ARG)`; the body streams
343
+ * on and fetch frames it afresh. */
344
+ const HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'te', 'trailer', 'upgrade', 'expect'];
345
+ const TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
346
+ function forwardableHeaders(incoming: Headers): Headers {
347
+ const headers = new Headers(incoming);
348
+ for (const listed of (incoming.get('connection') ?? '').split(',')) { const name = listed.trim(); if (TOKEN.test(name)) headers.delete(name); }
349
+ for (const h of HOP_BY_HOP) headers.delete(h);
350
+ return headers;
351
+ }
352
+
339
353
  /** Start the ambient redirect proxy. Returns a handle with the listen port + the env to export.
340
354
  * - Plain `http://` proxied requests to a vendor host → forwarded to the twin.
341
355
  * - `CONNECT host:443` to a vendor host → MITM-terminated with a session-signed leaf, decrypted, and
@@ -503,8 +517,8 @@ export async function startRedirectProxy(options: RedirectProxyOptions): Promise
503
517
  }
504
518
  const origin = new URL(twin.origin);
505
519
 
506
- const headers = new Headers(req.headers);
507
- for (const h of ['proxy-connection', 'accept-encoding', 'connection', 'x-volter-world-browser']) headers.delete(h);
520
+ const headers = forwardableHeaders(req.headers);
521
+ for (const h of ['accept-encoding', 'x-volter-world-browser']) headers.delete(h);
508
522
  // the application is not a twin: it gets no World key, keeps its Host and hears it was reached over TLS
509
523
  if (twin.vendor === 'app') {
510
524
  headers.delete('x-twins-key');
@@ -529,7 +543,7 @@ export async function startRedirectProxy(options: RedirectProxyOptions): Promise
529
543
  for (const h of ['content-encoding', 'content-length', 'transfer-encoding', 'connection', 'keep-alive']) out.delete(h);
530
544
  return new Response(upRes.body, { status: upRes.status, headers: out });
531
545
  } catch (err) {
532
- return new Response(twinUnreachable(twin.vendor, origin.origin, err as Error), { status: 502 });
546
+ return new Response(twinUnreachable(twin.vendor, origin.origin, err as Error), { status: 502, headers: { 'content-type': 'text/plain; charset=utf-8' } });
533
547
  }
534
548
  },
535
549
  upgrade: websocketRelay(req => {