@volter/world-runtime 3.0.101 → 3.0.102

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.
@@ -20,10 +20,19 @@ export async function processBirth(pid) {
20
20
  } catch { return undefined; }
21
21
  }
22
22
  return new Promise(resolve => {
23
- const child = spawn('ps', ['-o', 'pid=,lstart=', '-p', String(pid)], {
24
- windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'],
25
- env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' },
26
- });
23
+ // A probe that cannot run is a birth unknown, never a lifecycle failure: spawn throws synchronously where the
24
+ // process may not exec `ps` at all (setuid root on macOS, which a sandbox-exec profile refuses: EPERM), and reports
25
+ // other failures as 'error' (below). Either way the identity is the pid without a start time.
26
+ let child;
27
+ try {
28
+ child = spawn('ps', ['-o', 'pid=,lstart=', '-p', String(pid)], {
29
+ windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'],
30
+ env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' },
31
+ });
32
+ } catch {
33
+ resolve(undefined);
34
+ return;
35
+ }
27
36
  let output = '', settled = false;
28
37
  const finish = value => { if (!settled) { settled = true; clearTimeout(timer); child.stdout.destroy(); resolve(value); } };
29
38
  // This is our still-held inspector, not a PID from a receipt.
@@ -55,6 +55,33 @@ function listening(port) {
55
55
  socket.on('timeout', () => { socket.destroy(); resolve(false); });
56
56
  });
57
57
  }
58
+ /** Whether the Redis answering on the port is this child (architecture D9): its own INFO server names its process_id, as
59
+ * Redis's does. A port that answers as another process, or does not answer yet, is not this child's. */
60
+ function redisServes(port, pid) {
61
+ return new Promise((resolve) => {
62
+ const socket = connect({ host: '127.0.0.1', port, timeout: 1000 });
63
+ let reply = '';
64
+ socket.once('connect', () => socket.write('*2\r\n$4\r\nINFO\r\n$6\r\nserver\r\n'));
65
+ socket.on('data', (chunk) => {
66
+ reply += chunk.toString('latin1');
67
+ // a bulk string: $<length>\r\n<body>\r\n
68
+ const head = /^\$(\d+)\r\n/.exec(reply);
69
+ if (!head) {
70
+ if (!reply.startsWith('$') || reply.includes('\r\n')) {
71
+ socket.destroy();
72
+ resolve(false);
73
+ }
74
+ return;
75
+ }
76
+ if (Buffer.byteLength(reply, 'latin1') < head[0].length + Number(head[1]) + 2)
77
+ return;
78
+ socket.destroy();
79
+ resolve(Number(/(?:^|\r\n)process_id:(\d+)\r\n/.exec(reply.slice(head[0].length))?.[1]) === pid);
80
+ });
81
+ socket.on('error', () => resolve(false));
82
+ socket.on('timeout', () => { socket.destroy(); resolve(false); });
83
+ });
84
+ }
58
85
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
59
86
  export async function redisTwinUp(services, dataDir, cli, launch = {}) {
60
87
  for (const service of services) {
@@ -99,7 +126,7 @@ export async function redisTwinUp(services, dataDir, cli, launch = {}) {
99
126
  publishProcessReceipt(spawnReceiptPath(pidFile), JSON.stringify(identity) + '\n');
100
127
  publishProcessReceipt(pidFile, `${held.child.pid}\n`);
101
128
  const deadline = Date.now() + READY_TIMEOUT_MS;
102
- while (!(await listening(service.hostPort)) || !alive(held.child.pid)) {
129
+ while (!alive(held.child.pid) || !(await redisServes(service.hostPort, held.child.pid))) {
103
130
  if (!alive(held.child.pid))
104
131
  throw new Error(`redis exited before serving — see ${log}`);
105
132
  launch.signal?.throwIfAborted();
@@ -129,7 +156,8 @@ export async function redisTwinStatus(services, dataDir) {
129
156
  const pidFile = pidPath(dataDir, service);
130
157
  if (!existsSync(pidFile))
131
158
  continue;
132
- if (alive(Number(readFileSync(pidFile, 'utf8').trim())) && await listening(service.hostPort))
159
+ const pid = Number(readFileSync(pidFile, 'utf8').trim());
160
+ if (alive(pid) && await redisServes(service.hostPort, pid))
133
161
  ready += 1;
134
162
  }
135
163
  return ready;
@@ -3056,7 +3056,7 @@ function canonicalDottedIpv4(host) {
3056
3056
  function parseCustomShareOrigin(handshake) {
3057
3057
  const raw = /^https?:\/\/(\[[0-9a-f:.]+\]|[a-z0-9.-]+)(?::([0-9]{1,5}))?\/?$/i.exec(handshake);
3058
3058
  if (!raw) {
3059
- throw new Error(`Custom tunnel VOLTER_SHARE_URL must be a strict http(s) origin without path, query, fragment, or backslashes: ${handshake}`);
3059
+ throw new Error(`The URL a custom tunnel wrote to VOLTER_SHARE_URL_FILE must be a strict http(s) origin without path, query, fragment, or backslashes: ${handshake}`);
3060
3060
  }
3061
3061
  const rawHost = raw[1];
3062
3062
  const rawPort = raw[2];
@@ -3065,24 +3065,27 @@ function parseCustomShareOrigin(handshake) {
3065
3065
  parsed = new URL(handshake);
3066
3066
  }
3067
3067
  catch {
3068
- throw new Error(`Custom tunnel emitted an invalid VOLTER_SHARE_URL handshake: ${handshake}`);
3068
+ throw new Error(`A custom tunnel wrote an invalid URL to VOLTER_SHARE_URL_FILE: ${handshake}`);
3069
3069
  }
3070
3070
  const normalizedHostname = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase();
3071
3071
  if (!rawHost.startsWith('[') && isIP(normalizedHostname) === 4
3072
3072
  && (!canonicalDottedIpv4(rawHost) || rawHost !== normalizedHostname)) {
3073
- throw new Error(`Custom tunnel VOLTER_SHARE_URL must use canonical dotted-decimal IPv4 syntax: ${handshake}`);
3073
+ throw new Error(`The URL a custom tunnel wrote to VOLTER_SHARE_URL_FILE must use canonical dotted-decimal IPv4 syntax: ${handshake}`);
3074
3074
  }
3075
3075
  if (rawPort && rawPort.length > 1 && rawPort.startsWith('0')) {
3076
- throw new Error(`Custom tunnel VOLTER_SHARE_URL must use canonical decimal port syntax: ${handshake}`);
3076
+ throw new Error(`The URL a custom tunnel wrote to VOLTER_SHARE_URL_FILE must use canonical decimal port syntax: ${handshake}`);
3077
3077
  }
3078
3078
  if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || parsed.username || parsed.password
3079
3079
  || parsed.pathname !== '/' || parsed.search || parsed.hash) {
3080
- throw new Error(`Custom tunnel VOLTER_SHARE_URL must be a credential-free http(s) origin: ${handshake}`);
3080
+ throw new Error(`The URL a custom tunnel wrote to VOLTER_SHARE_URL_FILE must be a credential-free http(s) origin: ${handshake}`);
3081
3081
  }
3082
3082
  return parsed.origin;
3083
3083
  }
3084
- async function waitForPublicUrl(child, logPath, timeoutMs, requireTryCloudflare) {
3085
- const urlPattern = /https?:\/\/[^\s"'<>]+/gi;
3084
+ /** The tunnel's public URL, from the tunnel's own answer and never its output (architecture D9). cloudflared's metrics
3085
+ * server answers GET /quicktunnel with {"hostname"} once the quick tunnel is registered (empty before); a custom command
3086
+ * writes its public URL to VOLTER_SHARE_URL_FILE, ending the line when it is complete, so a half-written URL is never
3087
+ * read. The process's log is for people. */
3088
+ async function waitForPublicUrl(child, source, logPath, timeoutMs) {
3086
3089
  let exited = false;
3087
3090
  let exitSummary = '';
3088
3091
  child.once('exit', (code, signal) => {
@@ -3095,26 +3098,35 @@ async function waitForPublicUrl(child, logPath, timeoutMs, requireTryCloudflare)
3095
3098
  });
3096
3099
  const started = Date.now();
3097
3100
  while (Date.now() - started < timeoutMs) {
3098
- const text = existsSync(logPath) ? readFileSync(logPath, 'utf8') : '';
3099
- if (requireTryCloudflare) {
3100
- for (const candidate of text.match(urlPattern) ?? []) {
3101
- const parsed = parseCloudflareQuickPublicUrl(candidate);
3102
- if (parsed)
3101
+ if ('metricsPort' in source) {
3102
+ try {
3103
+ const answer = await fetch(`http://127.0.0.1:${source.metricsPort}/quicktunnel`, { signal: AbortSignal.timeout(1000) });
3104
+ const hostname = answer.ok ? (await answer.json()).hostname : undefined;
3105
+ if (typeof hostname === 'string' && hostname) {
3106
+ const parsed = parseCloudflareQuickPublicUrl(`https://${hostname}`);
3107
+ if (!parsed)
3108
+ throw new Error(`cloudflared answered /quicktunnel with ${JSON.stringify(hostname)}, not a trycloudflare.com hostname. Log: ${logPath}`);
3103
3109
  return parsed;
3110
+ }
3111
+ }
3112
+ catch (error) {
3113
+ if (error instanceof Error && error.message.startsWith('cloudflared answered'))
3114
+ throw error;
3115
+ // its metrics server not listening yet: asked again
3104
3116
  }
3105
3117
  }
3106
3118
  else {
3107
- const handshake = /^VOLTER_SHARE_URL=(\S+)\s*$/m.exec(text)?.[1];
3108
- if (handshake)
3109
- return parseCustomShareOrigin(handshake);
3119
+ const text = existsSync(source.urlFile) ? readFileSync(source.urlFile, 'utf8') : '';
3120
+ if (text.endsWith('\n'))
3121
+ return parseCustomShareOrigin(text.trim());
3110
3122
  }
3111
3123
  if (exited)
3112
- throw new Error(`Tunnel process exited before printing a public URL (${exitSummary}). Log: ${logPath}`);
3113
- await new Promise((resolveWait) => setTimeout(resolveWait, 250));
3124
+ throw new Error(`Tunnel process exited before answering its public URL (${exitSummary}). Log: ${logPath}`);
3125
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
3114
3126
  }
3115
- throw new Error(requireTryCloudflare
3116
- ? `Cloudflare tunnel did not print a trycloudflare.com https URL within ${timeoutMs}ms. Log: ${logPath}`
3117
- : `Custom tunnel did not print VOLTER_SHARE_URL=<http(s)-url> within ${timeoutMs}ms. Log: ${logPath}`);
3127
+ throw new Error('metricsPort' in source
3128
+ ? `cloudflared's metrics server did not answer a trycloudflare.com hostname at /quicktunnel within ${timeoutMs}ms. Log: ${logPath}`
3129
+ : `Custom tunnel did not write its public URL, newline-terminated, to VOLTER_SHARE_URL_FILE within ${timeoutMs}ms. Log: ${logPath}`);
3118
3130
  }
3119
3131
  function sleep(ms) {
3120
3132
  return new Promise((resolveWait) => setTimeout(resolveWait, ms));
@@ -3284,12 +3296,19 @@ async function shareWorldReserved(name, options) {
3284
3296
  }
3285
3297
  const tunnel = shareCommand(options, service.url);
3286
3298
  const log = ownedPath(instance.dirs.logs, `${serviceId}.tunnel.log`);
3287
- // Each spawned tunnel owns one log generation. Truncating before spawn prevents a repeated
3288
- // share from accepting the previous process's URL handshake as the new process's endpoint.
3299
+ // The public URL is the tunnel's own answer, never its output (architecture D9): cloudflared's metrics server
3300
+ // answers /quicktunnel with the hostname it registered; a custom command writes its URL to VOLTER_SHARE_URL_FILE.
3301
+ // Each spawned tunnel owns one generation of both, so a repeated share never takes the previous process's URL.
3302
+ const source = tunnel.provider === 'cloudflare-quick'
3303
+ ? { metricsPort: await allocatePort(new Set()) }
3304
+ : { urlFile: ownedPath(instance.dirs.logs, `${serviceId}.tunnel.url`) };
3305
+ if ('urlFile' in source)
3306
+ rmSync(source.urlFile, { force: true });
3307
+ const args = 'metricsPort' in source ? ['tunnel', '--metrics', `127.0.0.1:${source.metricsPort}`, ...tunnel.args.slice(1)] : tunnel.args;
3289
3308
  const out = openPrivateLog(log, 'w');
3290
- const child = spawn(tunnel.command, tunnel.args, { windowsHide: true,
3309
+ const child = spawn(tunnel.command, args, { windowsHide: true,
3291
3310
  cwd: root,
3292
- env: withoutVaultSignIn(process.env),
3311
+ env: { ...withoutVaultSignIn(process.env), ...('urlFile' in source ? { VOLTER_SHARE_URL_FILE: source.urlFile } : {}) },
3293
3312
  detached: true,
3294
3313
  stdio: ['ignore', out, out],
3295
3314
  });
@@ -3305,7 +3324,7 @@ async function shareWorldReserved(name, options) {
3305
3324
  if (!identity?.start)
3306
3325
  throw new Error('Tunnel birth capture failed; retiring held child');
3307
3326
  identities[identity.pid] = identity.start;
3308
- publicUrl = await waitForPublicUrl(child, log, options.timeoutMs ?? 20_000, tunnel.provider === 'cloudflare-quick');
3327
+ publicUrl = await waitForPublicUrl(child, source, log, options.timeoutMs ?? 20_000);
3309
3328
  if (options.verifyPath !== false) {
3310
3329
  publicVerification = await verifyPublicUrl(publicUrl, options.verifyPath ?? '/health', options.timeoutMs ?? 90_000);
3311
3330
  }
@@ -3324,7 +3343,7 @@ async function shareWorldReserved(name, options) {
3324
3343
  provider: tunnel.provider,
3325
3344
  pid: child.pid ?? 0,
3326
3345
  log,
3327
- command: [tunnel.command, ...tunnel.args],
3346
+ command: [tunnel.command, ...args],
3328
3347
  startedAt: new Date().toISOString(),
3329
3348
  };
3330
3349
  writePidsFromInstance(nextInstance, child.pid ? [child.pid] : []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.101",
3
+ "version": "3.0.102",
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",
@@ -69,14 +69,14 @@
69
69
  "node": ">=22.3"
70
70
  },
71
71
  "peerDependencies": {
72
- "@volter/world-core": "3.0.101",
73
- "@volter/world-console": "3.0.96"
72
+ "@volter/world-core": "3.0.102",
73
+ "@volter/world-console": "3.0.97"
74
74
  },
75
75
  "dependencies": {
76
76
  "@electric-sql/pglite": "0.5.8",
77
77
  "@electric-sql/pglite-pgvector": "0.0.9",
78
78
  "@volter/world-access": "3.0.1",
79
- "@volter/world-core": "3.0.101",
79
+ "@volter/world-core": "3.0.102",
80
80
  "mongodb": "~6.20.0",
81
81
  "pg-gateway": "0.3.0-beta.4",
82
82
  "smol-toml": "^1.8.0",
@@ -20,10 +20,19 @@ export async function processBirth(pid) {
20
20
  } catch { return undefined; }
21
21
  }
22
22
  return new Promise(resolve => {
23
- const child = spawn('ps', ['-o', 'pid=,lstart=', '-p', String(pid)], {
24
- windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'],
25
- env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' },
26
- });
23
+ // A probe that cannot run is a birth unknown, never a lifecycle failure: spawn throws synchronously where the
24
+ // process may not exec `ps` at all (setuid root on macOS, which a sandbox-exec profile refuses: EPERM), and reports
25
+ // other failures as 'error' (below). Either way the identity is the pid without a start time.
26
+ let child;
27
+ try {
28
+ child = spawn('ps', ['-o', 'pid=,lstart=', '-p', String(pid)], {
29
+ windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'],
30
+ env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' },
31
+ });
32
+ } catch {
33
+ resolve(undefined);
34
+ return;
35
+ }
27
36
  let output = '', settled = false;
28
37
  const finish = value => { if (!settled) { settled = true; clearTimeout(timer); child.stdout.destroy(); resolve(value); } };
29
38
  // This is our still-held inspector, not a PID from a receipt.
@@ -54,6 +54,27 @@ function listening(port: number): Promise<boolean> {
54
54
  });
55
55
  }
56
56
 
57
+ /** Whether the Redis answering on the port is this child (architecture D9): its own INFO server names its process_id, as
58
+ * Redis's does. A port that answers as another process, or does not answer yet, is not this child's. */
59
+ function redisServes(port: number, pid: number): Promise<boolean> {
60
+ return new Promise((resolve) => {
61
+ const socket = connect({ host: '127.0.0.1', port, timeout: 1000 });
62
+ let reply = '';
63
+ socket.once('connect', () => socket.write('*2\r\n$4\r\nINFO\r\n$6\r\nserver\r\n'));
64
+ socket.on('data', (chunk: Buffer) => {
65
+ reply += chunk.toString('latin1');
66
+ // a bulk string: $<length>\r\n<body>\r\n
67
+ const head = /^\$(\d+)\r\n/.exec(reply);
68
+ if (!head) { if (!reply.startsWith('$') || reply.includes('\r\n')) { socket.destroy(); resolve(false); } return; }
69
+ if (Buffer.byteLength(reply, 'latin1') < head[0].length + Number(head[1]) + 2) return;
70
+ socket.destroy();
71
+ resolve(Number(/(?:^|\r\n)process_id:(\d+)\r\n/.exec(reply.slice(head[0].length))?.[1]) === pid);
72
+ });
73
+ socket.on('error', () => resolve(false));
74
+ socket.on('timeout', () => { socket.destroy(); resolve(false); });
75
+ });
76
+ }
77
+
57
78
  const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
58
79
 
59
80
  export async function redisTwinUp(services: InfraService[], dataDir: string, cli: string, launch: HostLaunch = {}): Promise<void> {
@@ -93,7 +114,7 @@ export async function redisTwinUp(services: InfraService[], dataDir: string, cli
93
114
  publishProcessReceipt(spawnReceiptPath(pidFile), JSON.stringify(identity) + '\n');
94
115
  publishProcessReceipt(pidFile, `${held.child.pid}\n`);
95
116
  const deadline = Date.now() + READY_TIMEOUT_MS;
96
- while (!(await listening(service.hostPort)) || !alive(held.child.pid)) {
117
+ while (!alive(held.child.pid) || !(await redisServes(service.hostPort, held.child.pid))) {
97
118
  if (!alive(held.child.pid)) throw new Error(`redis exited before serving — see ${log}`);
98
119
  launch.signal?.throwIfAborted();
99
120
  if (Date.now() > deadline) throw new Error(`redis did not serve port ${service.hostPort} within ${READY_TIMEOUT_MS / 1000}s — see ${log}`);
@@ -116,7 +137,8 @@ export async function redisTwinStatus(services: InfraService[], dataDir: string)
116
137
  for (const service of services) {
117
138
  const pidFile = pidPath(dataDir, service);
118
139
  if (!existsSync(pidFile)) continue;
119
- if (alive(Number(readFileSync(pidFile, 'utf8').trim())) && await listening(service.hostPort)) ready += 1;
140
+ const pid = Number(readFileSync(pidFile, 'utf8').trim());
141
+ if (alive(pid) && await redisServes(service.hostPort, pid)) ready += 1;
120
142
  }
121
143
  return ready;
122
144
  }
package/src/runtime.ts CHANGED
@@ -2865,30 +2865,36 @@ function canonicalDottedIpv4(host: string): boolean {
2865
2865
  function parseCustomShareOrigin(handshake: string): string {
2866
2866
  const raw = /^https?:\/\/(\[[0-9a-f:.]+\]|[a-z0-9.-]+)(?::([0-9]{1,5}))?\/?$/i.exec(handshake);
2867
2867
  if (!raw) {
2868
- throw new Error(`Custom tunnel VOLTER_SHARE_URL must be a strict http(s) origin without path, query, fragment, or backslashes: ${handshake}`);
2868
+ throw new Error(`The URL a custom tunnel wrote to VOLTER_SHARE_URL_FILE must be a strict http(s) origin without path, query, fragment, or backslashes: ${handshake}`);
2869
2869
  }
2870
2870
  const rawHost = raw[1]!;
2871
2871
  const rawPort = raw[2];
2872
2872
  let parsed: URL;
2873
2873
  try { parsed = new URL(handshake); }
2874
- catch { throw new Error(`Custom tunnel emitted an invalid VOLTER_SHARE_URL handshake: ${handshake}`); }
2874
+ catch { throw new Error(`A custom tunnel wrote an invalid URL to VOLTER_SHARE_URL_FILE: ${handshake}`); }
2875
2875
  const normalizedHostname = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase();
2876
2876
  if (!rawHost.startsWith('[') && isIP(normalizedHostname) === 4
2877
2877
  && (!canonicalDottedIpv4(rawHost) || rawHost !== normalizedHostname)) {
2878
- throw new Error(`Custom tunnel VOLTER_SHARE_URL must use canonical dotted-decimal IPv4 syntax: ${handshake}`);
2878
+ throw new Error(`The URL a custom tunnel wrote to VOLTER_SHARE_URL_FILE must use canonical dotted-decimal IPv4 syntax: ${handshake}`);
2879
2879
  }
2880
2880
  if (rawPort && rawPort.length > 1 && rawPort.startsWith('0')) {
2881
- throw new Error(`Custom tunnel VOLTER_SHARE_URL must use canonical decimal port syntax: ${handshake}`);
2881
+ throw new Error(`The URL a custom tunnel wrote to VOLTER_SHARE_URL_FILE must use canonical decimal port syntax: ${handshake}`);
2882
2882
  }
2883
2883
  if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || parsed.username || parsed.password
2884
2884
  || parsed.pathname !== '/' || parsed.search || parsed.hash) {
2885
- throw new Error(`Custom tunnel VOLTER_SHARE_URL must be a credential-free http(s) origin: ${handshake}`);
2885
+ throw new Error(`The URL a custom tunnel wrote to VOLTER_SHARE_URL_FILE must be a credential-free http(s) origin: ${handshake}`);
2886
2886
  }
2887
2887
  return parsed.origin;
2888
2888
  }
2889
2889
 
2890
- async function waitForPublicUrl(child: ReturnType<typeof spawn>, logPath: string, timeoutMs: number, requireTryCloudflare: boolean): Promise<string> {
2891
- const urlPattern = /https?:\/\/[^\s"'<>]+/gi;
2890
+ /** Where a tunnel answers its public URL: cloudflared's metrics server, or the file a custom command writes. */
2891
+ type PublicUrlSource = { metricsPort: number } | { urlFile: string };
2892
+
2893
+ /** The tunnel's public URL, from the tunnel's own answer and never its output (architecture D9). cloudflared's metrics
2894
+ * server answers GET /quicktunnel with {"hostname"} once the quick tunnel is registered (empty before); a custom command
2895
+ * writes its public URL to VOLTER_SHARE_URL_FILE, ending the line when it is complete, so a half-written URL is never
2896
+ * read. The process's log is for people. */
2897
+ async function waitForPublicUrl(child: ReturnType<typeof spawn>, source: PublicUrlSource, logPath: string, timeoutMs: number): Promise<string> {
2892
2898
  let exited = false;
2893
2899
  let exitSummary = '';
2894
2900
  child.once('exit', (code, signal) => {
@@ -2902,22 +2908,29 @@ async function waitForPublicUrl(child: ReturnType<typeof spawn>, logPath: string
2902
2908
 
2903
2909
  const started = Date.now();
2904
2910
  while (Date.now() - started < timeoutMs) {
2905
- const text = existsSync(logPath) ? readFileSync(logPath, 'utf8') : '';
2906
- if (requireTryCloudflare) {
2907
- for (const candidate of text.match(urlPattern) ?? []) {
2908
- const parsed = parseCloudflareQuickPublicUrl(candidate);
2909
- if (parsed) return parsed;
2911
+ if ('metricsPort' in source) {
2912
+ try {
2913
+ const answer = await fetch(`http://127.0.0.1:${source.metricsPort}/quicktunnel`, { signal: AbortSignal.timeout(1000) });
2914
+ const hostname = answer.ok ? ((await answer.json()) as { hostname?: unknown }).hostname : undefined;
2915
+ if (typeof hostname === 'string' && hostname) {
2916
+ const parsed = parseCloudflareQuickPublicUrl(`https://${hostname}`);
2917
+ if (!parsed) throw new Error(`cloudflared answered /quicktunnel with ${JSON.stringify(hostname)}, not a trycloudflare.com hostname. Log: ${logPath}`);
2918
+ return parsed;
2919
+ }
2920
+ } catch (error) {
2921
+ if (error instanceof Error && error.message.startsWith('cloudflared answered')) throw error;
2922
+ // its metrics server not listening yet: asked again
2910
2923
  }
2911
2924
  } else {
2912
- const handshake = /^VOLTER_SHARE_URL=(\S+)\s*$/m.exec(text)?.[1];
2913
- if (handshake) return parseCustomShareOrigin(handshake);
2925
+ const text = existsSync(source.urlFile) ? readFileSync(source.urlFile, 'utf8') : '';
2926
+ if (text.endsWith('\n')) return parseCustomShareOrigin(text.trim());
2914
2927
  }
2915
- if (exited) throw new Error(`Tunnel process exited before printing a public URL (${exitSummary}). Log: ${logPath}`);
2916
- await new Promise((resolveWait) => setTimeout(resolveWait, 250));
2928
+ if (exited) throw new Error(`Tunnel process exited before answering its public URL (${exitSummary}). Log: ${logPath}`);
2929
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
2917
2930
  }
2918
- throw new Error(requireTryCloudflare
2919
- ? `Cloudflare tunnel did not print a trycloudflare.com https URL within ${timeoutMs}ms. Log: ${logPath}`
2920
- : `Custom tunnel did not print VOLTER_SHARE_URL=<http(s)-url> within ${timeoutMs}ms. Log: ${logPath}`);
2931
+ throw new Error('metricsPort' in source
2932
+ ? `cloudflared's metrics server did not answer a trycloudflare.com hostname at /quicktunnel within ${timeoutMs}ms. Log: ${logPath}`
2933
+ : `Custom tunnel did not write its public URL, newline-terminated, to VOLTER_SHARE_URL_FILE within ${timeoutMs}ms. Log: ${logPath}`);
2921
2934
  }
2922
2935
 
2923
2936
  function sleep(ms: number): Promise<void> {
@@ -3090,12 +3103,18 @@ async function shareWorldReserved(name: string, options: ShareWorldOptions): Pro
3090
3103
 
3091
3104
  const tunnel = shareCommand(options, service.url);
3092
3105
  const log = ownedPath(instance.dirs.logs, `${serviceId}.tunnel.log`);
3093
- // Each spawned tunnel owns one log generation. Truncating before spawn prevents a repeated
3094
- // share from accepting the previous process's URL handshake as the new process's endpoint.
3106
+ // The public URL is the tunnel's own answer, never its output (architecture D9): cloudflared's metrics server
3107
+ // answers /quicktunnel with the hostname it registered; a custom command writes its URL to VOLTER_SHARE_URL_FILE.
3108
+ // Each spawned tunnel owns one generation of both, so a repeated share never takes the previous process's URL.
3109
+ const source: PublicUrlSource = tunnel.provider === 'cloudflare-quick'
3110
+ ? { metricsPort: await allocatePort(new Set()) }
3111
+ : { urlFile: ownedPath(instance.dirs.logs, `${serviceId}.tunnel.url`) };
3112
+ if ('urlFile' in source) rmSync(source.urlFile, { force: true });
3113
+ const args = 'metricsPort' in source ? ['tunnel', '--metrics', `127.0.0.1:${source.metricsPort}`, ...tunnel.args.slice(1)] : tunnel.args;
3095
3114
  const out = openPrivateLog(log, 'w');
3096
- const child = spawn(tunnel.command, tunnel.args, { windowsHide: true,
3115
+ const child = spawn(tunnel.command, args, { windowsHide: true,
3097
3116
  cwd: root,
3098
- env: withoutVaultSignIn(process.env),
3117
+ env: { ...withoutVaultSignIn(process.env), ...('urlFile' in source ? { VOLTER_SHARE_URL_FILE: source.urlFile } : {}) },
3099
3118
  detached: true,
3100
3119
  stdio: ['ignore', out, out],
3101
3120
  });
@@ -3111,7 +3130,7 @@ async function shareWorldReserved(name: string, options: ShareWorldOptions): Pro
3111
3130
  const identity = await birth;
3112
3131
  if (!identity?.start) throw new Error('Tunnel birth capture failed; retiring held child');
3113
3132
  identities[identity.pid] = identity.start;
3114
- publicUrl = await waitForPublicUrl(child, log, options.timeoutMs ?? 20_000, tunnel.provider === 'cloudflare-quick');
3133
+ publicUrl = await waitForPublicUrl(child, source, log, options.timeoutMs ?? 20_000);
3115
3134
  if (options.verifyPath !== false) {
3116
3135
  publicVerification = await verifyPublicUrl(publicUrl, options.verifyPath ?? '/health', options.timeoutMs ?? 90_000);
3117
3136
  }
@@ -3129,7 +3148,7 @@ async function shareWorldReserved(name: string, options: ShareWorldOptions): Pro
3129
3148
  provider: tunnel.provider,
3130
3149
  pid: child.pid ?? 0,
3131
3150
  log,
3132
- command: [tunnel.command, ...tunnel.args],
3151
+ command: [tunnel.command, ...args],
3133
3152
  startedAt: new Date().toISOString(),
3134
3153
  };
3135
3154
  writePidsFromInstance(nextInstance, child.pid ? [child.pid] : []);