@evomap/evolver-proxy 2.0.2 → 2.0.12
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/bin/evolver-proxy.d.ts +12 -0
- package/dist/bin/evolver-proxy.js +192 -59
- package/dist/daemon/proxyDaemon.js +10 -0
- package/dist/daemon/systemdNotifier.d.ts +2 -0
- package/dist/daemon/systemdNotifier.js +11 -1
- package/dist/lifecycle/claimNudge.js +1 -1
- package/dist/lifecycle/deployGuard.js +1 -1
- package/dist/lifecycle/legacyNodeId.js +1 -1
- package/dist/lifecycle/manager.js +1 -1
- package/dist/llm/bodyCapture.js +1 -1
- package/dist/llm/index.js +1 -1
- package/dist/llm/server.js +1 -1
- package/dist/llm/traceBackfill.js +1 -1
- package/dist/llm/traceConfig.js +1 -1
- package/dist/llm/traceControl.js +1 -1
- package/dist/llm/traceEnvelope.js +1 -1
- package/dist/llm/traceSink.js +1 -1
- package/dist/llm/traceUploadPayload.js +1 -1
- package/dist/llm/upstream.js +1 -1
- package/dist/router/cachePassthrough.js +1 -1
- package/dist/router/features.js +1 -1
- package/dist/router/index.js +1 -1
- package/dist/router/messagesRoute.js +1 -1
- package/dist/router/modelRouter.js +1 -1
- package/dist/router/providerRoutes.js +1 -1
- package/dist/router/sseScan.js +1 -1
- package/dist/selfUpdate/bootstrap.d.ts +106 -13
- package/dist/selfUpdate/bootstrap.js +3418 -176
- package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
- package/dist/selfUpdate/bootstrapReadiness.js +153 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
- package/dist/selfUpdate/executor.d.ts +17 -6
- package/dist/selfUpdate/executor.js +158 -58
- package/dist/selfUpdate/index.d.ts +2 -1
- package/dist/selfUpdate/index.js +2 -1
- package/dist/selfUpdate/migration.d.ts +80 -15
- package/dist/selfUpdate/migration.js +2513 -156
- package/dist/selfUpdate/policy.js +2 -8
- package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
- package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
- package/dist/selfUpdate/releaseBinary.d.ts +3 -0
- package/dist/selfUpdate/releaseBinary.js +46 -3
- package/dist/selfUpdate/transaction.d.ts +8 -0
- package/dist/selfUpdate/transaction.js +166 -18
- package/dist/selfUpdate/unixController.d.ts +8 -0
- package/dist/selfUpdate/unixController.js +364 -38
- package/dist/selfUpdate/windowsController.d.ts +14 -2
- package/dist/selfUpdate/windowsController.js +482 -103
- package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
- package/dist/selfUpdate/windowsUpdater.js +174 -7
- package/dist/sync/engine.js +1 -1
- package/package.json +3 -3
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// Enterprise/air-gap deployments should pin 'off' explicitly in their env file (decided by the
|
|
23
23
|
// private adapter, not core).
|
|
24
24
|
import { resolveSelfUpdatePublicKey } from './builtinKey.js';
|
|
25
|
-
import {
|
|
25
|
+
import { selfUpdateProcessTargetBindable } from './releaseBinary.js';
|
|
26
26
|
/** Resolve EVOLVER_SELF_UPDATE to a policy. Unset → 'auto' (still gated by supervisor + public key). Unrecognized → 'off' (fail-closed). */
|
|
27
27
|
export function resolveSelfUpdatePolicy(env = process.env) {
|
|
28
28
|
const raw = (env['EVOLVER_SELF_UPDATE'] ?? '').trim().toLowerCase();
|
|
@@ -61,13 +61,7 @@ function selfUpdatePublicKeyAvailable(env) {
|
|
|
61
61
|
* for a target that assembly would reject.
|
|
62
62
|
*/
|
|
63
63
|
function selfUpdateTargetBindable(env, execPath) {
|
|
64
|
-
|
|
65
|
-
resolveSelfUpdateTarget({ env, processExecPath: execPath });
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
64
|
+
return selfUpdateProcessTargetBindable({ env, processExecPath: execPath });
|
|
71
65
|
}
|
|
72
66
|
/**
|
|
73
67
|
* Resolve the policy that startup should actually run with. A default (unset) 'auto' without durable
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { util } from '@evomap/evolver-core';
|
|
3
|
+
export declare const RECOVERY_CHILD_START_GATE_ENV = "EVOLVER_INTERNAL_RECOVERY_CHILD_START_GATE";
|
|
4
|
+
export declare const DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS = 120000;
|
|
5
|
+
export type RecoveryChildStartGateRole = 'proxy-target' | 'windows-updater';
|
|
6
|
+
export interface PreparedRecoveryChildStartGate {
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
startupGateToken: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ConsumeRecoveryChildStartGateOptions {
|
|
11
|
+
descriptor?: number;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
parentPid?: number;
|
|
14
|
+
inspectParentProcess?: (parent: Pick<util.FileLockOwnerRecord, 'pid' | 'processStartIdentity'>) => util.FileLockOwnerProcessStatus;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Bind a one-shot child start gate to this exact parent PID generation.
|
|
18
|
+
* The token travels in both the environment capability and a private fd4 pipe;
|
|
19
|
+
* neither input is sufficient on its own.
|
|
20
|
+
*/
|
|
21
|
+
export declare function prepareRecoveryChildStartGate(env: NodeJS.ProcessEnv, role: RecoveryChildStartGateRole, expectedParent?: Pick<util.FileLockOwnerRecord, 'pid' | 'processStartIdentity'>): PreparedRecoveryChildStartGate;
|
|
22
|
+
/**
|
|
23
|
+
* Consume the gate before controller/worker dispatch or durable recovery.
|
|
24
|
+
* Invalid capability, wrong role/parent generation, EOF, malformed input and
|
|
25
|
+
* timeout all fail closed. The inherited capability is scrubbed on every path.
|
|
26
|
+
*/
|
|
27
|
+
export declare function consumeRecoveryChildStartGate(env: NodeJS.ProcessEnv, expectedRole: RecoveryChildStartGateRole | undefined, options?: ConsumeRecoveryChildStartGateOptions): Promise<boolean>;
|
|
28
|
+
/** Deliver fd4 only after the caller establishes its owned guardian or delegated authority. */
|
|
29
|
+
export declare function deliverRecoveryChildStartGate(child: ChildProcess, token: string, timeoutMs?: number): Promise<boolean>;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, createReadStream } from 'node:fs';
|
|
3
|
+
import { Socket } from 'node:net';
|
|
4
|
+
import { util } from '@evomap/evolver-core';
|
|
5
|
+
export const RECOVERY_CHILD_START_GATE_ENV = 'EVOLVER_INTERNAL_RECOVERY_CHILD_START_GATE';
|
|
6
|
+
const RECOVERY_CHILD_START_GATE_DESCRIPTOR = 4;
|
|
7
|
+
export const DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS = 120_000;
|
|
8
|
+
const MAX_RECOVERY_CHILD_START_GATE_CAPABILITY_BYTES = 1024;
|
|
9
|
+
const MAX_RECOVERY_CHILD_START_GATE_SIGNAL_BYTES = 128;
|
|
10
|
+
const BUN_READER_CANCEL_TIMEOUT_MS = 1_000;
|
|
11
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12
|
+
/**
|
|
13
|
+
* Bind a one-shot child start gate to this exact parent PID generation.
|
|
14
|
+
* The token travels in both the environment capability and a private fd4 pipe;
|
|
15
|
+
* neither input is sufficient on its own.
|
|
16
|
+
*/
|
|
17
|
+
export function prepareRecoveryChildStartGate(env, role, expectedParent) {
|
|
18
|
+
if (expectedParent?.pid !== undefined && expectedParent.pid !== process.pid) {
|
|
19
|
+
throw new Error('self_update_recovery_child_start_gate_parent_invalid');
|
|
20
|
+
}
|
|
21
|
+
const processStartIdentity = util.readFileLockProcessStartIdentity(process.pid);
|
|
22
|
+
if (!processStartIdentity
|
|
23
|
+
|| (expectedParent
|
|
24
|
+
&& !util.sameFileLockProcessStartIdentity(expectedParent.processStartIdentity, processStartIdentity))) {
|
|
25
|
+
throw new Error('self_update_recovery_child_start_gate_parent_invalid');
|
|
26
|
+
}
|
|
27
|
+
const startupGateToken = randomUUID();
|
|
28
|
+
const capability = {
|
|
29
|
+
v: 1,
|
|
30
|
+
role,
|
|
31
|
+
pid: process.pid,
|
|
32
|
+
processStartIdentity,
|
|
33
|
+
token: startupGateToken,
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
env: {
|
|
37
|
+
...env,
|
|
38
|
+
[RECOVERY_CHILD_START_GATE_ENV]: JSON.stringify(capability),
|
|
39
|
+
},
|
|
40
|
+
startupGateToken,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function clearRecoveryChildStartGate(env) {
|
|
44
|
+
delete env[RECOVERY_CHILD_START_GATE_ENV];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Consume the gate before controller/worker dispatch or durable recovery.
|
|
48
|
+
* Invalid capability, wrong role/parent generation, EOF, malformed input and
|
|
49
|
+
* timeout all fail closed. The inherited capability is scrubbed on every path.
|
|
50
|
+
*/
|
|
51
|
+
export async function consumeRecoveryChildStartGate(env, expectedRole, options = {}) {
|
|
52
|
+
const raw = env[RECOVERY_CHILD_START_GATE_ENV];
|
|
53
|
+
if (raw === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
const descriptor = options.descriptor ?? RECOVERY_CHILD_START_GATE_DESCRIPTOR;
|
|
56
|
+
try {
|
|
57
|
+
const capability = parseRecoveryChildStartGateCapability(raw, expectedRole, options.parentPid ?? process.ppid, options.inspectParentProcess ?? util.inspectFileLockOwnerProcess);
|
|
58
|
+
if (!capability) {
|
|
59
|
+
await closeDescriptorBestEffort(descriptor);
|
|
60
|
+
throw new Error('self_update_recovery_child_start_gate_invalid');
|
|
61
|
+
}
|
|
62
|
+
const accepted = await readExactGateToken(descriptor, capability.token, positiveTimeout(options.timeoutMs));
|
|
63
|
+
if (!accepted) {
|
|
64
|
+
throw new Error('self_update_recovery_child_start_gate_rejected');
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
clearRecoveryChildStartGate(env);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Deliver fd4 only after the caller establishes its owned guardian or delegated authority. */
|
|
73
|
+
export function deliverRecoveryChildStartGate(child, token, timeoutMs = DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS) {
|
|
74
|
+
if (!UUID_PATTERN.test(token))
|
|
75
|
+
return Promise.resolve(false);
|
|
76
|
+
const stream = child.stdio[RECOVERY_CHILD_START_GATE_DESCRIPTOR];
|
|
77
|
+
const writable = stream;
|
|
78
|
+
if (!writable
|
|
79
|
+
|| typeof writable.on !== 'function'
|
|
80
|
+
|| typeof writable.once !== 'function'
|
|
81
|
+
|| typeof writable.off !== 'function'
|
|
82
|
+
|| typeof writable.end !== 'function'
|
|
83
|
+
|| typeof writable.destroy !== 'function') {
|
|
84
|
+
return Promise.resolve(false);
|
|
85
|
+
}
|
|
86
|
+
const gate = writable;
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
let settled = false;
|
|
89
|
+
const finish = (accepted) => {
|
|
90
|
+
if (settled)
|
|
91
|
+
return;
|
|
92
|
+
settled = true;
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
gate.off('finish', onFinish);
|
|
95
|
+
gate.off('error', onError);
|
|
96
|
+
gate.off('close', onClose);
|
|
97
|
+
resolve(accepted);
|
|
98
|
+
};
|
|
99
|
+
const onFinish = () => { finish(true); };
|
|
100
|
+
const onError = () => { finish(false); };
|
|
101
|
+
const onClose = () => { finish(false); };
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
try {
|
|
104
|
+
gate.destroy();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// The bounded false result remains authoritative.
|
|
108
|
+
}
|
|
109
|
+
finish(false);
|
|
110
|
+
}, positiveTimeout(timeoutMs));
|
|
111
|
+
gate.once('finish', onFinish);
|
|
112
|
+
gate.once('error', onError);
|
|
113
|
+
gate.once('close', onClose);
|
|
114
|
+
gate.end(`${token}\n`);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function parseRecoveryChildStartGateCapability(raw, expectedRole, parentPid, inspectParentProcess) {
|
|
118
|
+
if (!raw || Buffer.byteLength(raw, 'utf8') > MAX_RECOVERY_CHILD_START_GATE_CAPABILITY_BYTES) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(raw);
|
|
123
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
124
|
+
return undefined;
|
|
125
|
+
const record = parsed;
|
|
126
|
+
const keys = Object.keys(record).sort();
|
|
127
|
+
if (keys.length !== 5
|
|
128
|
+
|| keys[0] !== 'pid'
|
|
129
|
+
|| keys[1] !== 'processStartIdentity'
|
|
130
|
+
|| keys[2] !== 'role'
|
|
131
|
+
|| keys[3] !== 'token'
|
|
132
|
+
|| keys[4] !== 'v'
|
|
133
|
+
|| record['v'] !== 1
|
|
134
|
+
|| (record['role'] !== 'proxy-target' && record['role'] !== 'windows-updater')
|
|
135
|
+
|| record['role'] !== expectedRole
|
|
136
|
+
|| !Number.isSafeInteger(record['pid'])
|
|
137
|
+
|| record['pid'] !== parentPid
|
|
138
|
+
|| typeof record['token'] !== 'string'
|
|
139
|
+
|| !UUID_PATTERN.test(record['token'])) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const processStartIdentity = util.parseFileLockProcessStartIdentity(record['processStartIdentity']);
|
|
143
|
+
if (!processStartIdentity)
|
|
144
|
+
return undefined;
|
|
145
|
+
const capability = {
|
|
146
|
+
v: 1,
|
|
147
|
+
role: record['role'],
|
|
148
|
+
pid: record['pid'],
|
|
149
|
+
processStartIdentity,
|
|
150
|
+
token: record['token'],
|
|
151
|
+
};
|
|
152
|
+
if (inspectParentProcess(capability) !== 'current')
|
|
153
|
+
return undefined;
|
|
154
|
+
return capability;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function readExactGateToken(descriptor, token, timeoutMs) {
|
|
161
|
+
if (typeof process.versions['bun'] === 'string') {
|
|
162
|
+
const bun = activeBunFileRuntime();
|
|
163
|
+
if (!bun)
|
|
164
|
+
return Promise.resolve(false);
|
|
165
|
+
return readExactGateTokenFromBunFile(bun, descriptor, token, timeoutMs);
|
|
166
|
+
}
|
|
167
|
+
const expected = Buffer.from(`${token}\n`, 'utf8');
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
let raw = Buffer.alloc(0);
|
|
170
|
+
let settled = false;
|
|
171
|
+
let stream;
|
|
172
|
+
try {
|
|
173
|
+
// Node's libuv pipe socket can cancel an outstanding silent read on
|
|
174
|
+
// destroy; fs.ReadStream leaves that read live on Windows after timeout.
|
|
175
|
+
stream = new Socket({ fd: descriptor, readable: true, writable: false });
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Regular-file descriptors are useful for deterministic parser tests;
|
|
179
|
+
// Node production fd4 is always the cancellable pipe path above.
|
|
180
|
+
stream = createReadStream('', { fd: descriptor, autoClose: true });
|
|
181
|
+
}
|
|
182
|
+
const finish = (accepted) => {
|
|
183
|
+
if (settled)
|
|
184
|
+
return;
|
|
185
|
+
settled = true;
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
stream.off('data', onData);
|
|
188
|
+
stream.off('end', onEnd);
|
|
189
|
+
stream.off('error', onError);
|
|
190
|
+
stream.off('close', onClose);
|
|
191
|
+
if (!stream.destroyed)
|
|
192
|
+
stream.destroy();
|
|
193
|
+
resolve(accepted);
|
|
194
|
+
};
|
|
195
|
+
const onData = (chunk) => {
|
|
196
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
|
|
197
|
+
if (bytes.byteLength > MAX_RECOVERY_CHILD_START_GATE_SIGNAL_BYTES - raw.byteLength) {
|
|
198
|
+
finish(false);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
raw = Buffer.concat([raw, bytes], raw.byteLength + bytes.byteLength);
|
|
202
|
+
if (!raw.equals(expected.subarray(0, raw.byteLength))) {
|
|
203
|
+
finish(false);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const onEnd = () => { finish(raw.equals(expected)); };
|
|
207
|
+
const onError = () => { finish(false); };
|
|
208
|
+
const onClose = () => { finish(false); };
|
|
209
|
+
const timer = setTimeout(() => { finish(false); }, timeoutMs);
|
|
210
|
+
stream.on('data', onData);
|
|
211
|
+
stream.once('end', onEnd);
|
|
212
|
+
stream.once('error', onError);
|
|
213
|
+
stream.once('close', onClose);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async function readExactGateTokenFromBunFile(bun, descriptor, token, timeoutMs) {
|
|
217
|
+
const expected = Buffer.from(`${token}\n`, 'utf8');
|
|
218
|
+
let reader;
|
|
219
|
+
try {
|
|
220
|
+
// Bun's net.Socket accepts an inherited Windows pipe descriptor but never
|
|
221
|
+
// observes its data or EOF. Bun.file(fd).stream() owns that pipe correctly
|
|
222
|
+
// and its reader cancellation also releases a silent outstanding read.
|
|
223
|
+
reader = bun.file(descriptor).stream().getReader();
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const timedOut = Symbol('recovery-child-start-gate-timeout');
|
|
229
|
+
let timer;
|
|
230
|
+
const timeout = new Promise((resolve) => {
|
|
231
|
+
timer = setTimeout(() => { resolve(timedOut); }, timeoutMs);
|
|
232
|
+
});
|
|
233
|
+
let raw = Buffer.alloc(0);
|
|
234
|
+
let completed = false;
|
|
235
|
+
try {
|
|
236
|
+
for (;;) {
|
|
237
|
+
const result = await Promise.race([
|
|
238
|
+
reader.read().catch(() => undefined),
|
|
239
|
+
timeout,
|
|
240
|
+
]);
|
|
241
|
+
if (result === timedOut || result === undefined)
|
|
242
|
+
return false;
|
|
243
|
+
if (result.done) {
|
|
244
|
+
completed = true;
|
|
245
|
+
return raw.equals(expected);
|
|
246
|
+
}
|
|
247
|
+
const bytes = Buffer.from(result.value);
|
|
248
|
+
if (bytes.byteLength > MAX_RECOVERY_CHILD_START_GATE_SIGNAL_BYTES - raw.byteLength) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
raw = Buffer.concat([raw, bytes], raw.byteLength + bytes.byteLength);
|
|
252
|
+
if (!raw.equals(expected.subarray(0, raw.byteLength)))
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
if (timer !== undefined)
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
if (!completed) {
|
|
260
|
+
try {
|
|
261
|
+
await cancelBunReaderBestEffort(reader);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Rejection remains authoritative after cancellation was attempted.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function activeBunFileRuntime() {
|
|
270
|
+
const bun = globalThis.Bun;
|
|
271
|
+
return bun && typeof bun.file === 'function'
|
|
272
|
+
? bun
|
|
273
|
+
: undefined;
|
|
274
|
+
}
|
|
275
|
+
async function cancelBunReaderBestEffort(reader) {
|
|
276
|
+
await new Promise((resolve) => {
|
|
277
|
+
let settled = false;
|
|
278
|
+
const finish = () => {
|
|
279
|
+
if (settled)
|
|
280
|
+
return;
|
|
281
|
+
settled = true;
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
resolve();
|
|
284
|
+
};
|
|
285
|
+
const timer = setTimeout(finish, BUN_READER_CANCEL_TIMEOUT_MS);
|
|
286
|
+
try {
|
|
287
|
+
void reader.cancel('self_update_recovery_child_start_gate_closed').then(finish, finish);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
finish();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async function closeDescriptorBestEffort(descriptor) {
|
|
295
|
+
if (typeof process.versions['bun'] === 'string') {
|
|
296
|
+
const bun = activeBunFileRuntime();
|
|
297
|
+
if (!bun)
|
|
298
|
+
return;
|
|
299
|
+
try {
|
|
300
|
+
const reader = bun.file(descriptor).stream().getReader();
|
|
301
|
+
await cancelBunReaderBestEffort(reader);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// The invalid capability remains authoritative.
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
closeSync(descriptor);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// The capability rejection remains authoritative.
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function positiveTimeout(value) {
|
|
316
|
+
return Number.isFinite(value) && (value ?? 0) > 0
|
|
317
|
+
? Math.floor(value)
|
|
318
|
+
: DEFAULT_RECOVERY_CHILD_START_GATE_TIMEOUT_MS;
|
|
319
|
+
}
|
|
@@ -36,6 +36,7 @@ export declare const MAX_TARBALL_BYTES: number;
|
|
|
36
36
|
* bombs fail before the extractor accumulates unbounded output in memory.
|
|
37
37
|
*/
|
|
38
38
|
export declare const MAX_EXTRACTED_TARBALL_BYTES: number;
|
|
39
|
+
export declare function isStandaloneReleaseBinaryName(name: string): boolean;
|
|
39
40
|
export declare function requiredVersionForRelease(raw: unknown): string;
|
|
40
41
|
export declare function releaseAssetName(platform?: NodeJS.Platform, arch?: NodeJS.Architecture): string;
|
|
41
42
|
export declare function resolveGithubReleaseManifest(directive: ForceUpdateDirective, opts?: ReleaseBinaryOptions): Promise<UpdateManifest>;
|
|
@@ -63,4 +64,6 @@ export declare function resolveSelfUpdateTarget(opts?: ReleaseBinaryOptions): {
|
|
|
63
64
|
path: string;
|
|
64
65
|
explicit: boolean;
|
|
65
66
|
};
|
|
67
|
+
export declare function assertSelfUpdateProcessTargetBound(options: ReleaseBinaryOptions, allowUnresolvedTarget?: boolean): void;
|
|
68
|
+
export declare function selfUpdateProcessTargetBindable(options: ReleaseBinaryOptions): boolean;
|
|
66
69
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { chmod, copyFile, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
|
-
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { basename, dirname, join, resolve, win32 } from 'node:path';
|
|
5
6
|
import { createGunzip } from 'node:zlib';
|
|
6
7
|
import { ops } from '@evomap/evolver-core';
|
|
7
8
|
import { SELF_UPDATE_FAILURE_CODES, SelfUpdateFailureError, selfUpdateFailure } from './failureCodes.js';
|
|
@@ -30,6 +31,10 @@ export const MAX_TARBALL_BYTES = 64 * 1024 * 1024;
|
|
|
30
31
|
* bombs fail before the extractor accumulates unbounded output in memory.
|
|
31
32
|
*/
|
|
32
33
|
export const MAX_EXTRACTED_TARBALL_BYTES = 128 * 1024 * 1024;
|
|
34
|
+
const SELF_UPDATE_RELEASE_BINARY_RE = /^evolver(?:\.exe|-(?:darwin-(?:arm64|x64)|linux-(?:arm64|x64)|windows-x64\.exe))?$/;
|
|
35
|
+
export function isStandaloneReleaseBinaryName(name) {
|
|
36
|
+
return SELF_UPDATE_RELEASE_BINARY_RE.test(name.toLowerCase());
|
|
37
|
+
}
|
|
33
38
|
export function requiredVersionForRelease(raw) {
|
|
34
39
|
try {
|
|
35
40
|
return ops.requireSelfUpdateVersion(raw, 'required_version');
|
|
@@ -209,10 +214,48 @@ export function resolveSelfUpdateTarget(opts = {}) {
|
|
|
209
214
|
// correctly even when this check runs on a POSIX host (shape probes, tests).
|
|
210
215
|
const segments = execPath.split(/[/\\]+/).filter(Boolean);
|
|
211
216
|
const name = (segments.length > 0 ? segments[segments.length - 1] : basename(execPath)).toLowerCase();
|
|
212
|
-
if (name
|
|
217
|
+
if (isStandaloneReleaseBinaryName(name))
|
|
213
218
|
return { path: execPath, explicit: false };
|
|
214
219
|
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.INSTALL_GUARD_UNREADABLE, `self_update_target_required:${execPath}`);
|
|
215
220
|
}
|
|
221
|
+
export function assertSelfUpdateProcessTargetBound(options, allowUnresolvedTarget = false) {
|
|
222
|
+
let targetPath;
|
|
223
|
+
try {
|
|
224
|
+
targetPath = resolveSelfUpdateTarget(options).path;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
if (allowUnresolvedTarget)
|
|
228
|
+
return;
|
|
229
|
+
throw new Error('self_update_process_target_mismatch');
|
|
230
|
+
}
|
|
231
|
+
const processExecPath = options.processExecPath ?? process.execPath;
|
|
232
|
+
try {
|
|
233
|
+
if (canonicalExecutablePath(processExecPath) !== canonicalExecutablePath(targetPath)) {
|
|
234
|
+
throw new Error('self_update_process_target_mismatch');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
throw new Error('self_update_process_target_mismatch');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
export function selfUpdateProcessTargetBindable(options) {
|
|
242
|
+
try {
|
|
243
|
+
assertSelfUpdateProcessTargetBound(options);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function canonicalExecutablePath(path) {
|
|
251
|
+
const canonical = realpathSync.native(path);
|
|
252
|
+
if (process.platform !== 'win32')
|
|
253
|
+
return resolve(canonical);
|
|
254
|
+
const withoutNamespace = canonical
|
|
255
|
+
.replace(/^\\\\\?\\UNC\\/i, '\\\\')
|
|
256
|
+
.replace(/^\\\\\?\\/i, '');
|
|
257
|
+
return win32.normalize(withoutNamespace).toLowerCase();
|
|
258
|
+
}
|
|
216
259
|
function releaseDownloadUrl(releaseUrl, version, assetName) {
|
|
217
260
|
let base;
|
|
218
261
|
try {
|
|
@@ -391,7 +434,7 @@ function shaForAsset(sums, assetName) {
|
|
|
391
434
|
}
|
|
392
435
|
function assertSafeExecutableTarget(targetPath, explicitTarget) {
|
|
393
436
|
const name = basename(targetPath).toLowerCase();
|
|
394
|
-
if ((name
|
|
437
|
+
if (!isStandaloneReleaseBinaryName(name) && !explicitTarget) {
|
|
395
438
|
throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.INSTALL_GUARD_NAME_MISMATCH, `unsafe_self_update_target:${targetPath}`);
|
|
396
439
|
}
|
|
397
440
|
}
|
|
@@ -71,9 +71,17 @@ export type SelfUpdateRecoveryOptions = Omit<DurableSelfUpdateOptions, 'currentV
|
|
|
71
71
|
};
|
|
72
72
|
export interface StableUnixRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
|
|
73
73
|
platform?: NodeJS.Platform;
|
|
74
|
+
/** Bootstrap uses create-only ownership; explicit install-service keeps replacement semantics. */
|
|
75
|
+
replaceExisting?: boolean;
|
|
76
|
+
artifactClaimPath?: string;
|
|
77
|
+
onArtifactPublished?: (path: string, claimPath: string) => void | Promise<void>;
|
|
74
78
|
}
|
|
75
79
|
export interface StableWindowsRecoveryControllerOptions extends SelfUpdateRecoveryOptions {
|
|
76
80
|
platform?: NodeJS.Platform;
|
|
81
|
+
/** Bootstrap uses create-only ownership; explicit install-service keeps replacement semantics. */
|
|
82
|
+
replaceExisting?: boolean;
|
|
83
|
+
artifactClaimPath?: string;
|
|
84
|
+
onArtifactPublished?: (path: string, claimPath: string) => void | Promise<void>;
|
|
77
85
|
}
|
|
78
86
|
export declare function inspectDurableSelfUpdate(options: SelfUpdateRecoveryOptions): Promise<SelfUpdateRecoveryResult>;
|
|
79
87
|
export declare function resolveStableUnixRecoveryControllerPath(options: StableUnixRecoveryControllerOptions): Promise<string>;
|