@volter/world-runtime 3.0.163 → 3.0.165
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/ancestry.js +2 -2
- package/dist/src/app-url.d.ts +5 -1
- package/dist/src/app-url.js +14 -1
- package/dist/src/branch.js +28 -8
- package/dist/src/browser-files.d.ts +5 -0
- package/dist/src/browser-files.js +14 -0
- package/dist/src/browsers.d.ts +2 -3
- package/dist/src/browsers.js +4 -4
- package/dist/src/catalog.d.ts +2 -1
- package/dist/src/catalog.js +9 -3
- package/dist/src/changeset.js +1 -1
- package/dist/src/cli-commands.js +71 -18
- package/dist/src/cli-load-clock.d.ts +1 -0
- package/dist/src/cli-load-clock.js +2 -0
- package/dist/src/cli.js +5 -1
- package/dist/src/clickhouse-backing.d.ts +2 -0
- package/dist/src/clickhouse-backing.js +145 -0
- package/dist/src/command-lifetime.d.mts +1 -1
- package/dist/src/command-lifetime.mjs +5 -2
- package/dist/src/configs.js +1 -1
- package/dist/src/consumers.js +1 -1
- package/dist/src/covers.d.ts +4 -2
- package/dist/src/covers.js +8 -4
- package/dist/src/go-mysql-binding.d.ts +19 -0
- package/dist/src/go-mysql-binding.js +51 -0
- package/dist/src/guest-processes.d.mts +1 -1
- package/dist/src/guest-processes.mjs +22 -14
- package/dist/src/host-cli.js +8 -1
- package/dist/src/host-ready.d.ts +16 -2
- package/dist/src/host-ready.js +66 -39
- package/dist/src/host-worker.js +7 -1
- package/dist/src/host.d.ts +26 -0
- package/dist/src/host.js +93 -3
- package/dist/src/infra-definition.d.ts +35 -0
- package/dist/src/infra-definition.js +146 -0
- package/dist/src/infra.d.ts +3 -0
- package/dist/src/infra.js +375 -105
- package/dist/src/init.d.ts +46 -0
- package/dist/src/init.js +446 -19
- package/dist/src/lifecycle-record.d.ts +5 -1
- package/dist/src/lifecycle-record.js +27 -4
- package/dist/src/local-branches.js +1 -1
- package/dist/src/mariadb-bootstrap.d.ts +32 -0
- package/dist/src/mariadb-bootstrap.js +90 -0
- package/dist/src/mariadb-supplier.d.ts +23 -0
- package/dist/src/mariadb-supplier.js +215 -0
- package/dist/src/mongodb-backing.d.ts +4 -0
- package/dist/src/mongodb-backing.js +71 -1
- package/dist/src/native-mariadb-backing.d.ts +13 -0
- package/dist/src/native-mariadb-backing.js +512 -0
- package/dist/src/native-postgres-backing.d.ts +4 -10
- package/dist/src/native-postgres-backing.js +428 -43
- package/dist/src/native-server-inputs.d.ts +16 -0
- package/dist/src/native-server-inputs.js +204 -0
- package/dist/src/origin.js +3 -3
- package/dist/src/original-inputs.d.ts +14 -0
- package/dist/src/original-inputs.js +84 -0
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pglite-backing.d.ts +41 -17
- package/dist/src/pglite-backing.js +213 -79
- package/dist/src/pglite-host.mjs +482 -10
- package/dist/src/process-groups.d.mts +6 -1
- package/dist/src/process-groups.d.ts +2 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/process-groups.mjs +82 -15
- package/dist/src/process-signal.d.mts +2 -2
- package/dist/src/process-signal.mjs +29 -9
- package/dist/src/project-inspect.d.ts +12 -0
- package/dist/src/project-inspect.js +41 -4
- package/dist/src/proxy-env.d.ts +34 -0
- package/dist/src/proxy-env.js +114 -0
- package/dist/src/redirect-proxy.d.ts +1 -31
- package/dist/src/redirect-proxy.js +3 -112
- package/dist/src/redis-backing.d.ts +6 -4
- package/dist/src/redis-backing.js +20 -168
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +503 -218
- package/dist/src/schema.d.ts +1 -0
- package/dist/src/schema.js +5 -3
- package/dist/src/served-world.js +3 -3
- package/dist/src/twin-backing.d.ts +47 -0
- package/dist/src/twin-backing.js +343 -0
- package/dist/src/up-cli.js +56 -3
- package/package.json +3 -3
- package/src/ancestry.ts +2 -2
- package/src/app-url.ts +9 -2
- package/src/branch.ts +26 -8
- package/src/browser-files.ts +15 -0
- package/src/browsers.ts +4 -4
- package/src/catalog.ts +9 -4
- package/src/changeset.ts +1 -1
- package/src/cli-commands.ts +85 -18
- package/src/cli-load-clock.ts +2 -0
- package/src/cli.ts +5 -1
- package/src/clickhouse-backing.ts +70 -0
- package/src/command-lifetime.d.mts +1 -1
- package/src/command-lifetime.mjs +5 -2
- package/src/configs.ts +1 -1
- package/src/consumers.ts +1 -1
- package/src/covers.ts +9 -4
- package/src/go-mysql-binding.ts +49 -0
- package/src/guest-processes.d.mts +1 -1
- package/src/guest-processes.mjs +22 -14
- package/src/host-cli.ts +7 -1
- package/src/host-ready.ts +62 -31
- package/src/host-worker.ts +6 -2
- package/src/host.ts +88 -6
- package/src/infra-definition.ts +135 -0
- package/src/infra.ts +250 -45
- package/src/init.ts +417 -18
- package/src/lifecycle-record.ts +18 -4
- package/src/local-branches.ts +1 -1
- package/src/mariadb-bootstrap.ts +107 -0
- package/src/mariadb-supplier.ts +199 -0
- package/src/mongodb-backing.ts +59 -1
- package/src/native-mariadb-backing.ts +418 -0
- package/src/native-postgres-backing.ts +353 -41
- package/src/native-server-inputs.ts +169 -0
- package/src/origin.ts +3 -3
- package/src/original-inputs.ts +72 -0
- package/src/pack-facts.ts +1 -1
- package/src/pglite-backing.ts +192 -80
- package/src/pglite-host.mjs +482 -10
- package/src/process-groups.d.mts +6 -1
- package/src/process-groups.mjs +82 -15
- package/src/process-groups.ts +2 -1
- package/src/process-signal.d.mts +2 -2
- package/src/process-signal.mjs +29 -9
- package/src/project-inspect.ts +37 -4
- package/src/proxy-env.ts +128 -0
- package/src/redirect-proxy.ts +3 -125
- package/src/redis-backing.ts +19 -136
- package/src/runtime.ts +342 -107
- package/src/schema.ts +5 -4
- package/src/served-world.ts +3 -3
- package/src/twin-backing.ts +300 -0
- package/src/up-cli.ts +52 -3
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
/** WHERE A LIFECYCLE SAYS WHAT IT DID TO A PROCESS. Every signal this runtime delivers, every stop and exit of a
|
|
5
|
+
* backing service, and every read of the process table that failed or was slow is one JSON line in the World's
|
|
6
|
+
* events file (logs/events.jsonl), so a stop that went wrong is read from the World's own log. A lifecycle with no
|
|
7
|
+
* World's log to write in (a preparation run alone) says the same line on stderr. An instrument: it changes no
|
|
8
|
+
* outcome and never throws. */
|
|
9
|
+
let eventsFile;
|
|
10
|
+
export function setWorldEventsFile(path) { eventsFile = path && existsSync(path.slice(0, path.lastIndexOf('/'))) ? path : undefined; }
|
|
11
|
+
export function worldEvent(record) {
|
|
12
|
+
let line;
|
|
13
|
+
try { line = `${JSON.stringify({ ...record, process: process.pid, at: new Date().toISOString() })}\n`; } catch { return; }
|
|
14
|
+
if (eventsFile) { try { appendFileSync(eventsFile, line, { mode: 0o600 }); return; } catch { /* said on stderr instead */ } }
|
|
15
|
+
try { process.stderr.write(`[world-event] ${line}`); } catch { /* an instrument */ }
|
|
16
|
+
}
|
|
17
|
+
/** A read of the process table that did not answer, or took longer than this, is said with its time and outcome. */
|
|
18
|
+
const PROBE_SAID_MS = 250;
|
|
19
|
+
export function sayProbe(command, startedAt, outcome) {
|
|
20
|
+
const ms = Date.now() - startedAt;
|
|
21
|
+
if (outcome === 'answered' && ms < PROBE_SAID_MS) return;
|
|
22
|
+
worldEvent({ event: 'world.probe', command, ms, outcome });
|
|
23
|
+
}
|
|
24
|
+
const probeOutcome = result => result.error ? `error ${result.error.code ?? result.error.message}` : result.signal ? `killed ${result.signal}` : result.status === 0 ? 'answered' : `exit ${result.status}`;
|
|
3
25
|
|
|
4
26
|
/** Inspect only groups already owned by the lifecycle. A dead leader does not prove that its
|
|
5
27
|
* descendants exited. Zombies hold no resources; unknown inspection remains live. This is
|
|
6
|
-
* retirement evidence, never an ownership token or a way to discover processes to kill.
|
|
7
|
-
|
|
28
|
+
* retirement evidence, never an ownership token or a way to discover processes to kill.
|
|
29
|
+
*
|
|
30
|
+
* `listing` is one reading of the process table shared by the questions of one decision: an empty object from the
|
|
31
|
+
* caller, which the first question that needs the table fills and the rest read. A status asked three questions of
|
|
32
|
+
* the same table in a row, each its own `ps` over every process (Rallly run 209 in a tab: `attach` ran `ps -axo`
|
|
33
|
+
* twelve times, 0.74 s, before it started the command). Without it every call reads the table itself, as before. */
|
|
34
|
+
export function survivingOwnedGroups(pids, listing) {
|
|
8
35
|
const candidates = pids.filter(pid => {
|
|
9
36
|
try { process.kill(process.platform === 'win32' ? pid : -pid, 0); return true; }
|
|
10
37
|
catch (error) {
|
|
@@ -14,8 +41,23 @@ export function survivingOwnedGroups(pids) {
|
|
|
14
41
|
}
|
|
15
42
|
});
|
|
16
43
|
if (!candidates.length || process.platform === 'win32') return candidates;
|
|
44
|
+
// Linux says a process's group and state in /proc/<pid>/stat, which is what `ps` itself reads there: the table is
|
|
45
|
+
// read from the files, with no process started to read it. Each call started a `ps` (a World's `up` and `attach`
|
|
46
|
+
// twenty of them between the two, about 40 ms each where a process is costly to start), to learn three fields.
|
|
47
|
+
// A table that cannot be read whole falls to `ps` below, as before.
|
|
48
|
+
// Shared by the questions of one decision, as the `ps` listing below is: `null` is "read and not whole".
|
|
49
|
+
if (listing && !('proc' in listing)) listing.proc = linuxProcessTable() ?? null;
|
|
50
|
+
const table = listing ? listing.proc : linuxProcessTable();
|
|
51
|
+
if (table) return candidates.filter(pid => survives(pid, table));
|
|
17
52
|
const options = { windowsHide: true, encoding: 'utf8', timeout: 1000, killSignal: 'SIGKILL', maxBuffer: 4 * 1024 * 1024 };
|
|
18
|
-
|
|
53
|
+
// One listing answers every question of one decision: the first that needs it runs `ps`, said as a probe once.
|
|
54
|
+
const read = () => {
|
|
55
|
+
const tableAsked = Date.now();
|
|
56
|
+
const listed = spawnSync('ps', ['-axo', 'pid=,pgid=,stat='], options);
|
|
57
|
+
sayProbe('ps -axo pid=,pgid=,stat=', tableAsked, probeOutcome(listed));
|
|
58
|
+
return listed;
|
|
59
|
+
};
|
|
60
|
+
const result = listing ? (listing.table ??= read()) : read();
|
|
19
61
|
// When the whole-table listing does not answer (a non-zero status or an error; which of the two it was in the
|
|
20
62
|
// run this was written for, Cal build 43, was not read, and no measurement says why), every candidate would read
|
|
21
63
|
// as live, a group of zombies among them. Where the system can list one group by its leader (Darwin's `ps -g`: "processes with the
|
|
@@ -24,7 +66,9 @@ export function survivingOwnedGroups(pids) {
|
|
|
24
66
|
if (result.status !== 0 || result.error) {
|
|
25
67
|
if (process.platform !== 'darwin') return candidates;
|
|
26
68
|
return candidates.filter(pid => {
|
|
69
|
+
const groupAsked = Date.now();
|
|
27
70
|
const own = spawnSync('ps', ['-o', 'pid=,pgid=,stat=', '-g', String(pid)], options);
|
|
71
|
+
sayProbe(`ps -g ${pid}`, groupAsked, own.status === 1 && !own.error && !own.signal && !own.stdout.trim() ? 'answered' : probeOutcome(own));
|
|
28
72
|
// Status 1 with no row is `ps` saying the group has no process; any other failure says nothing.
|
|
29
73
|
if (own.error || own.signal || (own.status !== 0 && !(own.status === 1 && !own.stdout.trim()))) return true;
|
|
30
74
|
const members = own.stdout.trim() ? own.stdout.trim().split('\n').map(line => line.trim().split(/\s+/)) : [];
|
|
@@ -40,18 +84,41 @@ export function survivingOwnedGroups(pids) {
|
|
|
40
84
|
});
|
|
41
85
|
}
|
|
42
86
|
const rows = result.stdout.trim().split('\n').map(line => line.trim().split(/\s+/));
|
|
43
|
-
return candidates.filter(pid =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
87
|
+
return candidates.filter(pid => survives(pid, rows));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether a group's leader or any member is in the table and not a zombie; a group the table omits is asked of the kernel. */
|
|
91
|
+
function survives(pid, rows) {
|
|
92
|
+
const members = rows.filter(([id, group]) => Number(group) === pid || Number(id) === pid);
|
|
93
|
+
// Visibility restrictions can omit a live group. Only ESRCH proves absence.
|
|
94
|
+
if (!members.length) {
|
|
95
|
+
for (const target of [-pid, pid]) {
|
|
96
|
+
try { process.kill(target, 0); return true; }
|
|
97
|
+
catch (error) { if (error.code !== 'ESRCH') return true; }
|
|
52
98
|
}
|
|
53
|
-
return
|
|
54
|
-
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return members.some(([, , state]) => !state || !state.startsWith('Z'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The process table as `ps -axo pid=,pgid=,stat=` gives it, read from /proc: [pid, pgid, state] rows; undefined
|
|
105
|
+
* where this is not Linux, /proc is not there, or the directory cannot be listed. A process that ends between the
|
|
106
|
+
* listing and its read is not in the table, as it would not be in a `ps` taken a moment later. */
|
|
107
|
+
function linuxProcessTable() {
|
|
108
|
+
if (process.platform !== 'linux' || !existsSync('/proc/self/stat')) return undefined;
|
|
109
|
+
let names;
|
|
110
|
+
try { names = readdirSync('/proc'); } catch { return undefined; }
|
|
111
|
+
const rows = [];
|
|
112
|
+
for (const name of names) {
|
|
113
|
+
if (!/^[0-9]+$/.test(name)) continue;
|
|
114
|
+
let stat;
|
|
115
|
+
try { stat = readFileSync(`/proc/${name}/stat`, 'utf8'); } catch { continue; }
|
|
116
|
+
// `pid (comm) state ppid pgrp …`: the command may hold spaces and parentheses, so the fields are read after its last `)`.
|
|
117
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
118
|
+
if (fields.length < 3) return undefined;
|
|
119
|
+
rows.push([name, fields[2], fields[0]]);
|
|
120
|
+
}
|
|
121
|
+
return rows.length ? rows : undefined;
|
|
55
122
|
}
|
|
56
123
|
|
|
57
124
|
/** Each running pid's start time; a pid with no process is absent. Printed in one fixed zone and locale, so the same
|
|
@@ -3,7 +3,7 @@ import type { ChildProcess } from 'node:child_process';
|
|
|
3
3
|
export function processBirth(pid: number): Promise<string | undefined>;
|
|
4
4
|
export function spawnIdentity(pid: number): Promise<SpawnIdentity>;
|
|
5
5
|
export function verifySpawnIdentity(identity: SpawnIdentity | undefined): Promise<void>;
|
|
6
|
-
export function signalOwnedProcess(identity: SpawnIdentity | undefined, signal: NodeJS.Signals, group?: boolean): Promise<boolean>;
|
|
7
|
-
export function signalChild(child: ChildProcess, signal: NodeJS.Signals, group?: boolean): boolean;
|
|
6
|
+
export function signalOwnedProcess(identity: SpawnIdentity | undefined, signal: NodeJS.Signals, group?: boolean, by?: string): Promise<boolean>;
|
|
7
|
+
export function signalChild(child: ChildProcess, signal: NodeJS.Signals, group?: boolean, by?: string): boolean;
|
|
8
8
|
export function signalHeldChild(child: ChildProcess, signal: NodeJS.Signals, group?: boolean, birth?: Promise<SpawnIdentity | undefined>): Promise<boolean>;
|
|
9
9
|
export function childIdentity(child: ChildProcess): Promise<SpawnIdentity | undefined>;
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { survivingOwnedGroups } from './process-groups.mjs';
|
|
4
|
+
import { survivingOwnedGroups, worldEvent, sayProbe } from './process-groups.mjs';
|
|
5
|
+
|
|
6
|
+
/** Who asked for a signal, where the caller did not say: the first frame outside this file. */
|
|
7
|
+
function askedBy(by) {
|
|
8
|
+
if (by) return by;
|
|
9
|
+
const frame = (new Error().stack ?? '').split('\n').slice(2).find(line => !line.includes('process-signal.')) ?? '';
|
|
10
|
+
return frame.trim().replace(/^at\s+/, '').replace(/\(?(?:file:\/\/)?[^()\s]*\/([^/()\s]+:\d+):\d+\)?$/, '$1').trim() || 'unknown';
|
|
11
|
+
}
|
|
12
|
+
/** EVERY SIGNAL THIS RUNTIME DELIVERS IS SAID: the pid, whether to it alone or to its group, the signal, who asked,
|
|
13
|
+
* and what the system answered. Signal 0 is a question, not a delivery, and is not said. */
|
|
14
|
+
function saySignal(pid, toGroup, signal, by, result) {
|
|
15
|
+
if (signal === 0) return;
|
|
16
|
+
worldEvent({ event: 'world.signal', signal, pid, to: toGroup ? 'group' : 'pid', by: askedBy(by), result });
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
// Retain the existing 1000 ms inspection ceiling, not a new timeout chosen from one sample.
|
|
7
20
|
// Measured 2026-10-03 on shared Darwin 25.4.0 ARM64, Bun 1.3.11 (process.version
|
|
@@ -75,9 +88,11 @@ export async function processBirth(pid) {
|
|
|
75
88
|
return;
|
|
76
89
|
}
|
|
77
90
|
let output = '', settled = false;
|
|
91
|
+
const probeAsked = Date.now();
|
|
78
92
|
const finish = (value, error, code, signal, timedOut, reason) => {
|
|
79
93
|
if (settled) return;
|
|
80
94
|
settled = true;
|
|
95
|
+
sayProbe(`${probe.command} ${probe.args.join(' ')}`, probeAsked, value === undefined ? `${reason}${timedOut ? ' (timed out)' : ''}` : 'answered');
|
|
81
96
|
clearTimeout(timer);
|
|
82
97
|
child.stdout.destroy();
|
|
83
98
|
if (value === undefined) sayProbeFailure(probe, error, code, signal, timedOut, reason);
|
|
@@ -96,7 +111,7 @@ export async function processBirth(pid) {
|
|
|
96
111
|
// exited pid>` prints nothing and exits 1; its manual gives `-p` no other way to say it. Everything else is
|
|
97
112
|
// the probe failing: it could not be spawned, was killed, ran out of time, exited otherwise, or printed
|
|
98
113
|
// something that is not a birth. That is said once and is also undefined.
|
|
99
|
-
if (code === 1 && signal === null && output.trim() === '') { if (!settled) { settled = true; clearTimeout(timer); child.stdout.destroy(); resolve(undefined); } return; }
|
|
114
|
+
if (code === 1 && signal === null && output.trim() === '') { if (!settled) { settled = true; clearTimeout(timer); child.stdout.destroy(); sayProbe(`${probe.command} ${probe.args.join(' ')}`, probeAsked, 'answered'); resolve(undefined); } return; }
|
|
100
115
|
finish(code === 0 ? probe.read(output.trim()) : undefined, undefined, code, signal, false,
|
|
101
116
|
code === 0 ? 'unrecognized birth output' : 'nonzero exit');
|
|
102
117
|
});
|
|
@@ -145,21 +160,26 @@ function saySignalFailure(error, target, signal, child) {
|
|
|
145
160
|
} catch { /* the failure itself is still thrown by the caller */ }
|
|
146
161
|
}
|
|
147
162
|
/** Stored or retained numbers use birth verification before every delivery. */
|
|
148
|
-
export async function signalOwnedProcess(identity, signal, group = false) {
|
|
163
|
+
export async function signalOwnedProcess(identity, signal, group = false, by = undefined) {
|
|
164
|
+
const asker = askedBy(by);
|
|
149
165
|
await verifySpawnIdentity(identity);
|
|
150
166
|
const target = group && process.platform !== 'win32' ? -identity.pid : identity.pid;
|
|
151
|
-
try {
|
|
152
|
-
catch (error) {
|
|
167
|
+
try { const delivered = process.kill(target, signal); saySignal(identity.pid, target < 0, signal, asker, 'delivered'); return delivered; }
|
|
168
|
+
catch (error) {
|
|
169
|
+
saySignal(identity.pid, target < 0, signal, asker, error.code ?? 'failed');
|
|
170
|
+
if (error.code === 'ESRCH' || nothingLeftToSignal(identity.pid)) return false; saySignalFailure(error, target, signal); throw error;
|
|
171
|
+
}
|
|
153
172
|
}
|
|
154
173
|
/** A live ChildProcess is ownership until exit. A group additionally needs its leader alive.
|
|
155
174
|
* False means no held delivery was possible; a caller with surviving descendants needs a birth. */
|
|
156
|
-
export function signalChild(child, signal, group = false) {
|
|
175
|
+
export function signalChild(child, signal, group = false, by = undefined) {
|
|
157
176
|
if (!child.pid || child.exitCode !== null || child.signalCode !== null) return false;
|
|
158
|
-
|
|
177
|
+
const asker = askedBy(by);
|
|
178
|
+
if (!group || process.platform === 'win32') { const delivered = child.kill(signal); saySignal(child.pid, false, signal, asker, delivered ? 'delivered' : 'not delivered'); return delivered; }
|
|
159
179
|
try { process.kill(child.pid, 0); }
|
|
160
180
|
catch (error) { if (error.code === 'ESRCH' || nothingLeftToSignal(child.pid)) return false; saySignalFailure(error, child.pid, 0, child); throw error; }
|
|
161
|
-
try {
|
|
162
|
-
catch (error) { if (error.code === 'ESRCH' || nothingLeftToSignal(child.pid)) return false; saySignalFailure(error, -child.pid, signal, child); throw error; }
|
|
181
|
+
try { const delivered = process.kill(-child.pid, signal); saySignal(child.pid, true, signal, asker, 'delivered'); return delivered; }
|
|
182
|
+
catch (error) { saySignal(child.pid, true, signal, asker, error.code ?? 'failed'); if (error.code === 'ESRCH' || nothingLeftToSignal(child.pid)) return false; saySignalFailure(error, -child.pid, signal, child); throw error; }
|
|
163
183
|
}
|
|
164
184
|
|
|
165
185
|
/** Held first; an orphaned group then becomes retained numeric ownership. */
|
|
@@ -57,6 +57,18 @@ export declare function nestedProjectRoots(root: string): string[];
|
|
|
57
57
|
* env-name census: the root, its declared workspace members, and nested project
|
|
58
58
|
* roots (plus THEIR members). Single source of truth for the walkers below. */
|
|
59
59
|
export declare function projectManifestDirs(root: string): string[];
|
|
60
|
+
export declare function readOnlyWhatTheSourceHolds(root: string | undefined): void;
|
|
61
|
+
/** The files left unread since the rule was set, relative to the project, sorted. */
|
|
62
|
+
export declare function filesLeftAsThePersonsOwn(): string[];
|
|
63
|
+
/**
|
|
64
|
+
* THE TEXT OF A PROJECT'S VALUE FILE, AS THE ONE READER OF IT. With no rule set, the folder's file. With the rule
|
|
65
|
+
* set (a World made for an image), the text the SOURCE holds and nothing else: the committed blob (`git cat-file
|
|
66
|
+
* blob HEAD:./<file>`), never the folder's copy, which can carry a value a person typed and did not commit. The
|
|
67
|
+
* source of a folder is the repository that encloses it, its own or one above: git is asked from the folder. A file
|
|
68
|
+
* never committed, and every file of a folder under no repository, is the person's own: undefined, and recorded
|
|
69
|
+
* as left. A file's name decides nothing.
|
|
70
|
+
*/
|
|
71
|
+
export declare function sourceText(path: string): string | undefined;
|
|
60
72
|
/** Every dependency name declared anywhere in the repo — root manifest plus all
|
|
61
73
|
* workspace members, across dependencies/devDependencies/peerDependencies — plus
|
|
62
74
|
* the python manifest candidates. Exported raw for `covers`, which must also see
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
3
|
import { parse as parseToml } from 'smol-toml';
|
|
3
4
|
import { basename, dirname, relative, resolve } from 'node:path';
|
|
@@ -224,6 +225,35 @@ function json(path) {
|
|
|
224
225
|
return undefined;
|
|
225
226
|
}
|
|
226
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* WHAT THE PROJECT'S SOURCE HOLDS (architecture, "A World made for an image reads only what the project's source
|
|
230
|
+
* holds"). Set by a World's init made for an image, it stands in front of every reader of a project's env files
|
|
231
|
+
* here and in init. Unset, every file is read, as a person's own init reads.
|
|
232
|
+
*/
|
|
233
|
+
let sourceOnly;
|
|
234
|
+
export function readOnlyWhatTheSourceHolds(root) {
|
|
235
|
+
sourceOnly = root === undefined ? undefined : { root: resolve(root), left: new Set() };
|
|
236
|
+
}
|
|
237
|
+
/** The files left unread since the rule was set, relative to the project, sorted. */
|
|
238
|
+
export function filesLeftAsThePersonsOwn() { return sourceOnly ? [...sourceOnly.left].sort() : []; }
|
|
239
|
+
/**
|
|
240
|
+
* THE TEXT OF A PROJECT'S VALUE FILE, AS THE ONE READER OF IT. With no rule set, the folder's file. With the rule
|
|
241
|
+
* set (a World made for an image), the text the SOURCE holds and nothing else: the committed blob (`git cat-file
|
|
242
|
+
* blob HEAD:./<file>`), never the folder's copy, which can carry a value a person typed and did not commit. The
|
|
243
|
+
* source of a folder is the repository that encloses it, its own or one above: git is asked from the folder. A file
|
|
244
|
+
* never committed, and every file of a folder under no repository, is the person's own: undefined, and recorded
|
|
245
|
+
* as left. A file's name decides nothing.
|
|
246
|
+
*/
|
|
247
|
+
export function sourceText(path) {
|
|
248
|
+
if (!sourceOnly)
|
|
249
|
+
return readFileSync(path, 'utf8');
|
|
250
|
+
const absolute = resolve(path);
|
|
251
|
+
const shown = spawnSync('git', ['-C', sourceOnly.root, 'cat-file', 'blob', `HEAD:./${relative(sourceOnly.root, absolute)}`], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
252
|
+
if (shown.status === 0)
|
|
253
|
+
return shown.stdout;
|
|
254
|
+
sourceOnly.left.add(relative(sourceOnly.root, absolute) || basename(absolute));
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
227
257
|
function filesAt(root) {
|
|
228
258
|
if (!existsSync(root))
|
|
229
259
|
return [];
|
|
@@ -276,7 +306,9 @@ export function projectEnvNames(input = process.cwd()) {
|
|
|
276
306
|
const envNames = new Set();
|
|
277
307
|
for (const dir of projectManifestDirs(root)) {
|
|
278
308
|
for (const name of filesAt(dir).filter((candidate) => candidate === '.env' || candidate.startsWith('.env.'))) {
|
|
279
|
-
const text =
|
|
309
|
+
const text = sourceText(resolve(dir, name));
|
|
310
|
+
if (text === undefined)
|
|
311
|
+
continue;
|
|
280
312
|
for (const line of text.split(/\r?\n/)) {
|
|
281
313
|
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
282
314
|
if (match)
|
|
@@ -302,8 +334,11 @@ function publicRegistryUrl(configured) {
|
|
|
302
334
|
function inspectedEnv(dir, label, onlyFile) {
|
|
303
335
|
const values = new Map();
|
|
304
336
|
const sources = new Map();
|
|
305
|
-
for (const name of filesAt(dir).filter((candidate) => onlyFile ? candidate === onlyFile : candidate === '.env' || candidate.startsWith('.env.')).sort()) {
|
|
306
|
-
|
|
337
|
+
for (const name of filesAt(dir).filter((candidate) => (onlyFile ? candidate === onlyFile : candidate === '.env' || candidate.startsWith('.env.'))).sort()) {
|
|
338
|
+
const text = sourceText(resolve(dir, name));
|
|
339
|
+
if (text === undefined)
|
|
340
|
+
continue;
|
|
341
|
+
for (const [index, line] of text.split(/\r?\n/).entries()) {
|
|
307
342
|
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
308
343
|
if (!match)
|
|
309
344
|
continue;
|
|
@@ -601,7 +636,9 @@ export function projectConnections(input = process.cwd(), attributedEnv = new Se
|
|
|
601
636
|
// Inspect each example/config separately: conflicting schemes must not silently overwrite
|
|
602
637
|
// one another. inspectedEnv is only used for resolving ordinary references within a file.
|
|
603
638
|
for (const file of filesAt(dir).filter((name) => name === '.env' || name.startsWith('.env.')).sort()) {
|
|
604
|
-
const text =
|
|
639
|
+
const text = sourceText(resolve(dir, file));
|
|
640
|
+
if (text === undefined)
|
|
641
|
+
continue;
|
|
605
642
|
const env = inspectedEnv(dir, label, file);
|
|
606
643
|
for (const [index, line] of text.split(/\r?\n/).entries()) {
|
|
607
644
|
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CaTrustInputs } from './ca-trust.js';
|
|
2
|
+
/** The vendor→twin map this world actively redirects (for diagnostics / activate output). */
|
|
3
|
+
export declare function activeVendorMap(env: Record<string, string | undefined>): Record<string, string>;
|
|
4
|
+
export type CaPaths = {
|
|
5
|
+
dir: string;
|
|
6
|
+
caKey: string;
|
|
7
|
+
caCert: string;
|
|
8
|
+
};
|
|
9
|
+
/** Whether this host opens raw sockets to other hosts, which the ambient TLS redirect proxy exists to intercept. A
|
|
10
|
+
* browser tab's Node (tabnode, which declares `process.versions.webcontainer`) opens none: its TLS is loopback pairing
|
|
11
|
+
* with no handshake, and a remote HTTPS call leaves through the page's HTTP transport. There no proxy can run and none
|
|
12
|
+
* is needed: the World's network policy is enforced by the injector in every Node program (world-core inject.cjs). */
|
|
13
|
+
export declare function hostHasRawEgress(versions?: Record<string, string | undefined>): boolean;
|
|
14
|
+
export declare function opensslAvailable(): boolean;
|
|
15
|
+
/** Generate (once) a throwaway CA in `tlsDir`. Idempotent: reuses an existing CA so a re-activate in
|
|
16
|
+
* the same world trusts the same cert. Session/project-scoped — it lives under the instance dir and
|
|
17
|
+
* is removed by `tearDownCa` on `down`. */
|
|
18
|
+
export declare function ensureCa(tlsDir: string): CaPaths;
|
|
19
|
+
/** Remove the session CA + any minted leaf certs. Called on `down` so the trusted CA never outlives
|
|
20
|
+
* the world (it was only ever trusted via the per-shell env, never system-wide). */
|
|
21
|
+
export declare function tearDownCa(tlsDir: string): void;
|
|
22
|
+
/** Mint (and cache) a leaf cert for `host`, signed by the session CA. SAN = the host so the client
|
|
23
|
+
* accepts it for that SNI. */
|
|
24
|
+
export declare function leafCertFor(host: string, ca: CaPaths): {
|
|
25
|
+
key: string;
|
|
26
|
+
cert: string;
|
|
27
|
+
};
|
|
28
|
+
/** The cert-trust + proxy env that points an unmodified tool through the proxy and trusts the session
|
|
29
|
+
* CA — and ONLY this session (these are per-shell exports; nothing is installed system-wide). Mirrors
|
|
30
|
+
* the env list in WORLD_ACTIVATE.md §"The honest cost: a scoped CA". */
|
|
31
|
+
export declare function proxyEnvFor(proxyUrl: string, caCertPath: string, caller?: CaTrustInputs, guestUrl?: string): Record<string, string>;
|
|
32
|
+
/** The proxy's address env; `VOLTER_WORLD_GUEST_PROXY` is its guests' listener, which a World Machine's forwarder
|
|
33
|
+
* targets (architecture, "The scoped proxy's local pass-through"). */
|
|
34
|
+
export declare function proxyAddressEnv(proxyUrl: string, caCertPath: string, guestUrl?: string): Record<string, string>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { loadInject } from "./inject-map.js";
|
|
6
|
+
import { sessionTrustEnv } from "./ca-trust.js";
|
|
7
|
+
/** The vendor→twin map this world actively redirects (for diagnostics / activate output). */
|
|
8
|
+
export function activeVendorMap(env) {
|
|
9
|
+
return loadInject().readMap(env);
|
|
10
|
+
}
|
|
11
|
+
function caPaths(tlsDir) {
|
|
12
|
+
return { dir: tlsDir, caKey: join(tlsDir, 'ca-key.pem'), caCert: join(tlsDir, 'ca-cert.pem') };
|
|
13
|
+
}
|
|
14
|
+
/** Whether this host opens raw sockets to other hosts, which the ambient TLS redirect proxy exists to intercept. A
|
|
15
|
+
* browser tab's Node (tabnode, which declares `process.versions.webcontainer`) opens none: its TLS is loopback pairing
|
|
16
|
+
* with no handshake, and a remote HTTPS call leaves through the page's HTTP transport. There no proxy can run and none
|
|
17
|
+
* is needed: the World's network policy is enforced by the injector in every Node program (world-core inject.cjs). */
|
|
18
|
+
export function hostHasRawEgress(versions = process.versions) {
|
|
19
|
+
return versions.webcontainer === undefined;
|
|
20
|
+
}
|
|
21
|
+
export function opensslAvailable() {
|
|
22
|
+
// TEST-ONLY seam (TWIN-64): forces "openssl unavailable" deterministically. A PATH-shim (fake
|
|
23
|
+
// `openssl` on PATH) doesn't reliably work here because `spawnSync('openssl', …)` below is called
|
|
24
|
+
// with no explicit `env`, and some runtimes resolve the executable against a PATH snapshotted at
|
|
25
|
+
// process start rather than a same-process `process.env.PATH` mutation made mid-test. Never set
|
|
26
|
+
// this env var in real usage.
|
|
27
|
+
if (process.env.VOLTER_TEST_NO_OPENSSL === '1')
|
|
28
|
+
return false;
|
|
29
|
+
const r = spawnSync('openssl', ['version'], { windowsHide: true, stdio: 'ignore' });
|
|
30
|
+
return r.status === 0;
|
|
31
|
+
}
|
|
32
|
+
function runOpenssl(args) {
|
|
33
|
+
const r = spawnSync('openssl', args, { windowsHide: true, encoding: 'utf8' });
|
|
34
|
+
if (r.status !== 0)
|
|
35
|
+
throw new Error(`openssl ${args[0]} failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
36
|
+
return r.stdout;
|
|
37
|
+
}
|
|
38
|
+
/** Generate (once) a throwaway CA in `tlsDir`. Idempotent: reuses an existing CA so a re-activate in
|
|
39
|
+
* the same world trusts the same cert. Session/project-scoped — it lives under the instance dir and
|
|
40
|
+
* is removed by `tearDownCa` on `down`. */
|
|
41
|
+
export function ensureCa(tlsDir) {
|
|
42
|
+
if (!opensslAvailable()) {
|
|
43
|
+
throw new Error('openssl not found — the TLS redirect proxy needs openssl to mint a session CA. Install openssl, or use per-CLI endpoint env (cliRedirect) instead.');
|
|
44
|
+
}
|
|
45
|
+
const paths = caPaths(tlsDir);
|
|
46
|
+
if (existsSync(paths.caKey) && existsSync(paths.caCert))
|
|
47
|
+
return paths;
|
|
48
|
+
mkdirSync(tlsDir, { recursive: true });
|
|
49
|
+
runOpenssl(['genrsa', '-out', paths.caKey, '2048']);
|
|
50
|
+
runOpenssl([
|
|
51
|
+
'req', '-x509', '-new', '-nodes', '-key', paths.caKey,
|
|
52
|
+
'-sha256', '-days', '3650', '-out', paths.caCert,
|
|
53
|
+
'-subj', '/CN=volter-world session CA/O=volter-world',
|
|
54
|
+
// A CA cert without keyUsage is refused by OpenSSL 3 (Python, curl on Debian 13: "CA cert does not
|
|
55
|
+
// include key usage extension"), so the CA states what a CA is for.
|
|
56
|
+
'-addext', 'basicConstraints=critical,CA:TRUE',
|
|
57
|
+
'-addext', 'keyUsage=critical,keyCertSign,cRLSign',
|
|
58
|
+
'-addext', 'subjectKeyIdentifier=hash',
|
|
59
|
+
]);
|
|
60
|
+
return paths;
|
|
61
|
+
}
|
|
62
|
+
/** Remove the session CA + any minted leaf certs. Called on `down` so the trusted CA never outlives
|
|
63
|
+
* the world (it was only ever trusted via the per-shell env, never system-wide). */
|
|
64
|
+
export function tearDownCa(tlsDir) {
|
|
65
|
+
leafCache.clear();
|
|
66
|
+
rmSync(tlsDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
const leafCache = new Map();
|
|
69
|
+
/** Mint (and cache) a leaf cert for `host`, signed by the session CA. SAN = the host so the client
|
|
70
|
+
* accepts it for that SNI. */
|
|
71
|
+
export function leafCertFor(host, ca) {
|
|
72
|
+
// A pathname can hold a replacement CA; its certificate identifies the signing generation.
|
|
73
|
+
const cacheKey = `${createHash('sha256').update(readFileSync(ca.caCert)).digest('hex')}:${host}`;
|
|
74
|
+
const cached = leafCache.get(cacheKey);
|
|
75
|
+
if (cached)
|
|
76
|
+
return cached;
|
|
77
|
+
const safe = host.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
78
|
+
const keyPath = join(ca.dir, `leaf-${safe}-key.pem`);
|
|
79
|
+
const csrPath = join(ca.dir, `leaf-${safe}.csr`);
|
|
80
|
+
const certPath = join(ca.dir, `leaf-${safe}.pem`);
|
|
81
|
+
const extPath = join(ca.dir, `leaf-${safe}.ext`);
|
|
82
|
+
runOpenssl(['genrsa', '-out', keyPath, '2048']);
|
|
83
|
+
runOpenssl(['req', '-new', '-key', keyPath, '-out', csrPath, '-subj', `/CN=${host}`]);
|
|
84
|
+
writeFileSync(extPath, `subjectAltName=DNS:${host}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n`);
|
|
85
|
+
runOpenssl([
|
|
86
|
+
// 397 days, NOT the CA's 3650: macOS's system verifier (used by Go — `gh`, and any Go/Swift
|
|
87
|
+
// client) rejects a LEAF cert valid >398 days as "certificate is not standards compliant"
|
|
88
|
+
// (CA/Browser-Forum max). The session CA itself is exempt and stays long-lived.
|
|
89
|
+
'x509', '-req', '-in', csrPath, '-CA', ca.caCert, '-CAkey', ca.caKey,
|
|
90
|
+
'-CAcreateserial', '-out', certPath, '-days', '397', '-sha256', '-extfile', extPath,
|
|
91
|
+
]);
|
|
92
|
+
const minted = { key: readFileSync(keyPath, 'utf8'), cert: readFileSync(certPath, 'utf8') };
|
|
93
|
+
leafCache.set(cacheKey, minted);
|
|
94
|
+
return minted;
|
|
95
|
+
}
|
|
96
|
+
/** The cert-trust + proxy env that points an unmodified tool through the proxy and trusts the session
|
|
97
|
+
* CA — and ONLY this session (these are per-shell exports; nothing is installed system-wide). Mirrors
|
|
98
|
+
* the env list in WORLD_ACTIVATE.md §"The honest cost: a scoped CA". */
|
|
99
|
+
export function proxyEnvFor(proxyUrl, caCertPath, caller = process.env, guestUrl) {
|
|
100
|
+
return { ...proxyAddressEnv(proxyUrl, caCertPath, guestUrl), ...sessionTrustEnv(caCertPath, caller) };
|
|
101
|
+
}
|
|
102
|
+
/** The proxy's address env; `VOLTER_WORLD_GUEST_PROXY` is its guests' listener, which a World Machine's forwarder
|
|
103
|
+
* targets (architecture, "The scoped proxy's local pass-through"). */
|
|
104
|
+
export function proxyAddressEnv(proxyUrl, caCertPath, guestUrl) {
|
|
105
|
+
return {
|
|
106
|
+
...(guestUrl ? { VOLTER_WORLD_GUEST_PROXY: guestUrl } : {}),
|
|
107
|
+
HTTPS_PROXY: proxyUrl,
|
|
108
|
+
HTTP_PROXY: proxyUrl,
|
|
109
|
+
https_proxy: proxyUrl,
|
|
110
|
+
http_proxy: proxyUrl,
|
|
111
|
+
VOLTER_WORLD_PROXY: proxyUrl,
|
|
112
|
+
VOLTER_WORLD_CA: caCertPath,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, tearDownCa, leafCertFor, proxyEnvFor } from './proxy-env.js';
|
|
1
2
|
import { type CaTrustInputs } from './ca-trust.js';
|
|
2
3
|
/**
|
|
3
4
|
* The body served when a host was MITM'd but no twin serves the PATH.
|
|
@@ -28,8 +29,6 @@ export declare function proxyTargetFor(hostname: string, env: Record<string, str
|
|
|
28
29
|
vendor: string;
|
|
29
30
|
origin: string;
|
|
30
31
|
} | null;
|
|
31
|
-
/** The vendor→twin map this world actively redirects (for diagnostics / activate output). */
|
|
32
|
-
export declare function activeVendorMap(env: Record<string, string | undefined>): Record<string, string>;
|
|
33
32
|
/** A connection from anything but the proxy host's own loopback is a guest's (a World Machine: its forwarder connects
|
|
34
33
|
* from 127.0.0.2 on this host, or from its VM's address): architecture, "The scoped proxy's local pass-through". */
|
|
35
34
|
export declare function isGuestSource(address: string | undefined): boolean;
|
|
@@ -45,30 +44,6 @@ export declare function guestDestination(host: string): Promise<{
|
|
|
45
44
|
hostNetwork?: true;
|
|
46
45
|
address?: string;
|
|
47
46
|
}>;
|
|
48
|
-
type CaPaths = {
|
|
49
|
-
dir: string;
|
|
50
|
-
caKey: string;
|
|
51
|
-
caCert: string;
|
|
52
|
-
};
|
|
53
|
-
/** Whether this host opens raw sockets to other hosts, which the ambient TLS redirect proxy exists to intercept. A
|
|
54
|
-
* browser tab's Node (tabnode, which declares `process.versions.webcontainer`) opens none: its TLS is loopback pairing
|
|
55
|
-
* with no handshake, and a remote HTTPS call leaves through the page's HTTP transport. There no proxy can run and none
|
|
56
|
-
* is needed: the World's network policy is enforced by the injector in every Node program (world-core inject.cjs). */
|
|
57
|
-
export declare function hostHasRawEgress(versions?: Record<string, string | undefined>): boolean;
|
|
58
|
-
export declare function opensslAvailable(): boolean;
|
|
59
|
-
/** Generate (once) a throwaway CA in `tlsDir`. Idempotent: reuses an existing CA so a re-activate in
|
|
60
|
-
* the same world trusts the same cert. Session/project-scoped — it lives under the instance dir and
|
|
61
|
-
* is removed by `tearDownCa` on `down`. */
|
|
62
|
-
export declare function ensureCa(tlsDir: string): CaPaths;
|
|
63
|
-
/** Remove the session CA + any minted leaf certs. Called on `down` so the trusted CA never outlives
|
|
64
|
-
* the world (it was only ever trusted via the per-shell env, never system-wide). */
|
|
65
|
-
export declare function tearDownCa(tlsDir: string): void;
|
|
66
|
-
/** Mint (and cache) a leaf cert for `host`, signed by the session CA. SAN = the host so the client
|
|
67
|
-
* accepts it for that SNI. */
|
|
68
|
-
export declare function leafCertFor(host: string, ca: CaPaths): {
|
|
69
|
-
key: string;
|
|
70
|
-
cert: string;
|
|
71
|
-
};
|
|
72
47
|
type RedirectProxyOptions = {
|
|
73
48
|
/** The world env — supplies the `*_TWIN_URL` host→twin map. */
|
|
74
49
|
env?: Record<string, string | undefined>;
|
|
@@ -105,8 +80,3 @@ export declare function twinUnreachableMessage(vendor: string, origin: string, e
|
|
|
105
80
|
* - `CONNECT host:443` to a vendor host → MITM-terminated with a session-signed leaf, decrypted, and
|
|
106
81
|
* forwarded to the twin. CONNECT to any other host → blind-tunneled to the real origin untouched. */
|
|
107
82
|
export declare function startRedirectProxy(options: RedirectProxyOptions): Promise<RedirectProxyHandle>;
|
|
108
|
-
/** The cert-trust + proxy env that points an unmodified tool through the proxy and trusts the session
|
|
109
|
-
* CA — and ONLY this session (these are per-shell exports; nothing is installed system-wide). Mirrors
|
|
110
|
-
* the env list in WORLD_ACTIVATE.md §"The honest cost: a scoped CA". */
|
|
111
|
-
export declare function proxyEnvFor(proxyUrl: string, caCertPath: string, caller?: CaTrustInputs, guestUrl?: string): Record<string, string>;
|
|
112
|
-
export {};
|