@evomap/evolver-proxy 2.0.0-beta.1 → 2.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/bin/evolver-proxy.d.ts +67 -6
  2. package/dist/bin/evolver-proxy.js +389 -75
  3. package/dist/bin/proxySettings.d.ts +2 -0
  4. package/dist/bin/proxySettings.js +8 -1
  5. package/dist/daemon/collaborationFacade.d.ts +56 -0
  6. package/dist/daemon/collaborationFacade.js +877 -0
  7. package/dist/daemon/proxyDaemon.d.ts +3 -0
  8. package/dist/daemon/proxyDaemon.js +111 -0
  9. package/dist/daemon/selectHub.js +17 -1
  10. package/dist/llm/traceControl.js +1 -1
  11. package/dist/private/adapterLoader.d.ts +6 -1
  12. package/dist/private/adapterLoader.js +14 -3
  13. package/dist/router/messagesRoute.d.ts +13 -0
  14. package/dist/router/messagesRoute.js +56 -0
  15. package/dist/selfUpdate/executor.d.ts +10 -5
  16. package/dist/selfUpdate/executor.js +81 -6
  17. package/dist/selfUpdate/failureCodes.d.ts +6 -0
  18. package/dist/selfUpdate/failureCodes.js +6 -0
  19. package/dist/selfUpdate/index.d.ts +4 -1
  20. package/dist/selfUpdate/index.js +4 -1
  21. package/dist/selfUpdate/lastUpdate.d.ts +3 -1
  22. package/dist/selfUpdate/lastUpdate.js +37 -6
  23. package/dist/selfUpdate/releaseBinary.d.ts +10 -0
  24. package/dist/selfUpdate/releaseBinary.js +43 -6
  25. package/dist/selfUpdate/transaction.d.ts +109 -0
  26. package/dist/selfUpdate/transaction.js +1174 -0
  27. package/dist/selfUpdate/unixController.d.ts +15 -0
  28. package/dist/selfUpdate/unixController.js +186 -0
  29. package/dist/selfUpdate/version.d.ts +6 -2
  30. package/dist/selfUpdate/version.js +5 -3
  31. package/dist/selfUpdate/windowsController.d.ts +23 -0
  32. package/dist/selfUpdate/windowsController.js +274 -0
  33. package/dist/selfUpdate/windowsUpdater.d.ts +79 -0
  34. package/dist/selfUpdate/windowsUpdater.js +715 -0
  35. package/dist/sync/engine.d.ts +6 -5
  36. package/dist/sync/engine.js +102 -58
  37. package/package.json +8 -3
@@ -0,0 +1,715 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { link, lstat, mkdir, open, realpath, rename, rm, } from 'node:fs/promises';
4
+ import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
5
+ import { SELF_UPDATE_FAILURE_CODES, SelfUpdateFailureError, selfUpdateFailure, } from './failureCodes.js';
6
+ export const WINDOWS_UPDATER_WORKER_ARG = '--evolver-windows-updater-worker';
7
+ const WORK_ITEM_SCHEMA_VERSION = 1;
8
+ const RESULT_SCHEMA_VERSION = 1;
9
+ const MAX_WORK_ITEM_BYTES = 64 * 1024;
10
+ const COPY_BUFFER_BYTES = 1024 * 1024;
11
+ const OPERATION_ID_PATTERN = /^[0-9a-f]{32}$/;
12
+ /** Paths are fixed so the stable controller never consumes descriptor-supplied executable paths. */
13
+ export function resolveWindowsUpdaterPaths(stateDirInput) {
14
+ const stateDir = assertAbsoluteCleanPath(stateDirInput, 'state_dir');
15
+ const directory = join(stateDir, 'windows-updater');
16
+ return {
17
+ directory,
18
+ helperPath: join(directory, 'updater.exe'),
19
+ pendingPath: join(directory, 'pending.json'),
20
+ resultPath: join(directory, 'result.json'),
21
+ };
22
+ }
23
+ /** Bind a fixed executable below the private state root without following directory links. */
24
+ export async function bindWindowsManagedExecutable(options) {
25
+ assertWindowsPlatform(options.platform ?? process.platform);
26
+ if (options.relativePath.length === 0
27
+ || options.relativePath.some((segment) => !segment || segment === '.' || segment === '..' || basename(segment) !== segment)) {
28
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${options.label}_path_invalid`);
29
+ }
30
+ const stateDir = await assertPrivateDirectory(options.stateDir, 'state_dir', false);
31
+ const expectedPath = join(stateDir, ...options.relativePath);
32
+ await assertPrivateDirectory(dirname(expectedPath), `${options.label}_dir`, false);
33
+ const executablePath = await assertManagedRegularPath(options.executablePath, stateDir, options.label);
34
+ const canonicalExpectedPath = await assertManagedRegularPath(expectedPath, stateDir, `${options.label}_expected`);
35
+ if (!samePath(executablePath, canonicalExpectedPath)) {
36
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${options.mismatchLabel ?? options.label}_exec_mismatch`);
37
+ }
38
+ return { stateDir, executablePath };
39
+ }
40
+ /**
41
+ * Prepare a launcher-consumed update descriptor. This function never mutates
42
+ * the live executable and never spawns a competing relaunch process.
43
+ *
44
+ * The stable lifecycle controller runs updater.exe before it starts target.
45
+ * The worker applies pending.json while no target process exists, then removes
46
+ * pending.json only after an idempotently durable success result is written.
47
+ */
48
+ export async function prepareWindowsExecutableSwap(options) {
49
+ assertWindowsPlatform(options.platform ?? process.platform);
50
+ const stateDir = await assertPrivateDirectory(options.stateDir, 'state_dir', true);
51
+ const paths = resolveWindowsUpdaterPaths(stateDir);
52
+ await assertPrivateDirectory(paths.directory, 'updater_dir', true);
53
+ await assertPathAbsent(paths.pendingPath, 'pending');
54
+ const targetPath = await assertExternalRegularPath(options.targetPath, stateDir, 'target');
55
+ const backupPath = await assertManagedRegularPath(options.backupPath, stateDir, 'backup');
56
+ const targetIdentity = await snapshotRegularFile(targetPath, 'target');
57
+ const backupIdentity = await snapshotRegularFile(backupPath, 'backup');
58
+ let stagedPath;
59
+ let stagedIdentity;
60
+ let sourcePath;
61
+ let sourceIdentity;
62
+ let helperSourcePath;
63
+ if (options.operation === 'install') {
64
+ if (!options.stagedPath || !options.expectedStagedSha256) {
65
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_staged_required');
66
+ }
67
+ stagedPath = await assertManagedRegularPath(options.stagedPath, stateDir, 'staged');
68
+ stagedIdentity = await snapshotRegularFile(stagedPath, 'staged');
69
+ assertSha256(options.expectedStagedSha256, 'expected_staged_sha256');
70
+ if (!safeDigestEqual(stagedIdentity.sha256, options.expectedStagedSha256)) {
71
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REJECTED_VERIFICATION, 'windows_updater_staged_digest_mismatch');
72
+ }
73
+ sourcePath = stagedPath;
74
+ sourceIdentity = stagedIdentity;
75
+ // The new staged binary contains the worker, so this also bootstraps the
76
+ // first helper-capable release from an older executable.
77
+ helperSourcePath = stagedPath;
78
+ }
79
+ else {
80
+ if (options.stagedPath || options.expectedStagedSha256) {
81
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_rollback_staged_forbidden');
82
+ }
83
+ sourcePath = backupPath;
84
+ sourceIdentity = backupIdentity;
85
+ helperSourcePath = assertAbsoluteCleanPath(options.helperSourcePath ?? options.processExecPath ?? process.execPath, 'helper_source');
86
+ await assertNoSymlinkedParent(helperSourcePath, 'helper_source');
87
+ }
88
+ const helperSourceIdentity = await snapshotRegularFile(helperSourcePath, 'helper_source');
89
+ const operationId = randomBytes(16).toString('hex');
90
+ const helperTempPath = join(paths.directory, `.updater-${operationId}.tmp`);
91
+ try {
92
+ await copyRegularFileExclusive(helperSourcePath, helperTempPath, helperSourceIdentity, 0o700);
93
+ await commitPreparedHelper(helperTempPath, paths.helperPath, helperSourceIdentity);
94
+ }
95
+ catch (error) {
96
+ await removeBestEffort(helperTempPath);
97
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.COPY_FAILED, 'windows_updater_helper_prepare_failed', {
98
+ cause: error,
99
+ });
100
+ }
101
+ const helperIdentity = await snapshotRegularFile(paths.helperPath, 'helper');
102
+ if (!safeDigestEqual(helperIdentity.sha256, helperSourceIdentity.sha256)) {
103
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.COPY_FAILED, 'windows_updater_helper_digest_mismatch');
104
+ }
105
+ const workItem = {
106
+ schema_version: WORK_ITEM_SCHEMA_VERSION,
107
+ operation_id: operationId,
108
+ operation: options.operation,
109
+ target_path: targetPath,
110
+ ...(stagedPath ? { staged_path: stagedPath } : {}),
111
+ backup_path: backupPath,
112
+ source_path: sourcePath,
113
+ target_identity: targetIdentity,
114
+ ...(stagedIdentity ? { staged_identity: stagedIdentity } : {}),
115
+ backup_identity: backupIdentity,
116
+ source_identity: sourceIdentity,
117
+ helper_identity: helperIdentity,
118
+ };
119
+ // Publish pending.json last. link() makes the descriptor visible atomically
120
+ // and refuses to replace an existing pending operation.
121
+ await removeBestEffort(paths.resultPath);
122
+ await writeJsonNoReplaceAtomic(paths.pendingPath, workItem, 0o600);
123
+ return {
124
+ operation: options.operation,
125
+ operationId,
126
+ helperPath: paths.helperPath,
127
+ pendingPath: paths.pendingPath,
128
+ resultPath: paths.resultPath,
129
+ };
130
+ }
131
+ /**
132
+ * Apply the pending operation before the stable controller starts target.
133
+ * A successful swap is idempotent across crashes after rename: if target
134
+ * already has source's content, the helper only finalizes result/pending state.
135
+ */
136
+ export async function applyPendingWindowsExecutableSwap(options = {}) {
137
+ assertWindowsPlatform(options.platform ?? process.platform);
138
+ const workerExecPathInput = assertAbsoluteCleanPath(options.workerExecPath ?? process.execPath, 'worker_exec');
139
+ const inferredStateDir = dirname(dirname(workerExecPathInput));
140
+ const boundWorker = await bindWindowsManagedExecutable({
141
+ stateDir: options.stateDir ?? inferredStateDir,
142
+ executablePath: workerExecPathInput,
143
+ relativePath: ['windows-updater', 'updater.exe'],
144
+ label: 'worker_exec',
145
+ mismatchLabel: 'helper',
146
+ platform: options.platform,
147
+ });
148
+ const stateDir = boundWorker.stateDir;
149
+ const paths = resolveWindowsUpdaterPaths(stateDir);
150
+ let workItem;
151
+ try {
152
+ workItem = await readWorkItem(paths.pendingPath);
153
+ await validateWorkItem(workItem, paths, stateDir);
154
+ }
155
+ catch (error) {
156
+ const result = {
157
+ schema_version: RESULT_SCHEMA_VERSION,
158
+ operation: workItem?.operation ?? 'install',
159
+ status: 'failed',
160
+ failure_code: updaterFailureCode(error),
161
+ };
162
+ await writeJsonAtomic(paths.resultPath, result, 0o600);
163
+ return result;
164
+ }
165
+ if (!workItem) {
166
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_pending_missing');
167
+ }
168
+ let result;
169
+ try {
170
+ await assertSnapshot(workItem.source_path, workItem.source_identity, 'source');
171
+ await assertSnapshot(workItem.backup_path, workItem.backup_identity, 'backup');
172
+ if (workItem.staged_path && workItem.staged_identity) {
173
+ await assertSnapshot(workItem.staged_path, workItem.staged_identity, 'staged');
174
+ }
175
+ const target = await snapshotRegularFile(workItem.target_path, 'target');
176
+ if (!sameContent(target, workItem.source_identity)) {
177
+ if (!sameIdentity(target, workItem.target_identity)) {
178
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_target_changed');
179
+ }
180
+ const replacementPath = join(dirname(workItem.target_path), `.${workItem.operation_id}.evolver-replacement`);
181
+ await removeKnownReplacementTemp(replacementPath);
182
+ try {
183
+ await copyRegularFileExclusive(workItem.source_path, replacementPath, workItem.source_identity, 0o700);
184
+ await assertSnapshot(workItem.target_path, workItem.target_identity, 'target');
185
+ // Exactly one target mutation. The old fixed entry remains present until
186
+ // the verified sibling replacement is atomically installed.
187
+ await (options.renameFn ?? rename)(replacementPath, workItem.target_path);
188
+ }
189
+ catch (error) {
190
+ await removeBestEffort(replacementPath);
191
+ throw error;
192
+ }
193
+ }
194
+ result = {
195
+ schema_version: RESULT_SCHEMA_VERSION,
196
+ operation: workItem.operation,
197
+ status: 'completed',
198
+ };
199
+ await writeJsonAtomic(paths.resultPath, result, 0o600);
200
+ await rm(paths.pendingPath);
201
+ }
202
+ catch (error) {
203
+ result = {
204
+ schema_version: RESULT_SCHEMA_VERSION,
205
+ operation: workItem.operation,
206
+ status: 'failed',
207
+ failure_code: updaterFailureCode(error),
208
+ };
209
+ await writeJsonAtomic(paths.resultPath, result, 0o600);
210
+ }
211
+ return result;
212
+ }
213
+ /** Return undefined for normal execution, otherwise the launcher helper exit code. */
214
+ export async function maybeRunWindowsUpdaterWorkerFromArgv(options = {}) {
215
+ const argv = options.argv ?? process.argv.slice(2);
216
+ if (!argv.includes(WINDOWS_UPDATER_WORKER_ARG))
217
+ return undefined;
218
+ if ((options.platform ?? process.platform) !== 'win32')
219
+ return 64;
220
+ if (argv.length !== 2 || argv[0] !== 'proxy' || argv[1] !== WINDOWS_UPDATER_WORKER_ARG)
221
+ return 64;
222
+ try {
223
+ const result = await applyPendingWindowsExecutableSwap({
224
+ stateDir: options.env?.['EVOLVER_SELF_UPDATE_STATE_DIR']?.trim() || undefined,
225
+ platform: options.platform,
226
+ workerExecPath: options.processExecPath,
227
+ });
228
+ return result.status === 'completed' ? 0 : 1;
229
+ }
230
+ catch {
231
+ return 1;
232
+ }
233
+ }
234
+ async function commitPreparedHelper(helperTempPath, helperPath, sourceIdentity) {
235
+ try {
236
+ const info = await lstat(helperPath);
237
+ if (info.isSymbolicLink() || !info.isFile()) {
238
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_helper_unsafe');
239
+ }
240
+ const existing = await snapshotRegularFile(helperPath, 'helper');
241
+ if (sameContent(existing, sourceIdentity)) {
242
+ await rm(helperTempPath);
243
+ return;
244
+ }
245
+ await rm(helperPath);
246
+ }
247
+ catch (error) {
248
+ if (!isErrno(error, 'ENOENT'))
249
+ throw error;
250
+ }
251
+ await rename(helperTempPath, helperPath);
252
+ }
253
+ async function validateWorkItem(workItem, paths, stateDir) {
254
+ if (!OPERATION_ID_PATTERN.test(workItem.operation_id)) {
255
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_operation_id_invalid');
256
+ }
257
+ const targetPath = await assertExternalRegularPath(workItem.target_path, stateDir, 'target');
258
+ const backupPath = await assertManagedRegularPath(workItem.backup_path, stateDir, 'backup');
259
+ const sourcePath = await assertManagedRegularPath(workItem.source_path, stateDir, 'source');
260
+ if (samePath(targetPath, sourcePath) || samePath(targetPath, backupPath)) {
261
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_target_alias');
262
+ }
263
+ if (workItem.operation === 'install') {
264
+ if (!workItem.staged_path || !workItem.staged_identity) {
265
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_staged_missing');
266
+ }
267
+ const stagedPath = await assertManagedRegularPath(workItem.staged_path, stateDir, 'staged');
268
+ if (!samePath(stagedPath, sourcePath)) {
269
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_install_source_mismatch');
270
+ }
271
+ workItem.staged_path = stagedPath;
272
+ }
273
+ else if (workItem.staged_path || workItem.staged_identity || !samePath(sourcePath, backupPath)) {
274
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_rollback_source_mismatch');
275
+ }
276
+ workItem.target_path = targetPath;
277
+ workItem.backup_path = backupPath;
278
+ workItem.source_path = sourcePath;
279
+ await assertSnapshot(paths.helperPath, workItem.helper_identity, 'helper');
280
+ await assertSnapshot(backupPath, workItem.backup_identity, 'backup');
281
+ await assertSnapshot(sourcePath, workItem.source_identity, 'source');
282
+ }
283
+ async function readWorkItem(workItemPath) {
284
+ const snapshot = await snapshotRegularFile(workItemPath, 'pending', MAX_WORK_ITEM_BYTES);
285
+ if (BigInt(snapshot.size) > BigInt(MAX_WORK_ITEM_BYTES)) {
286
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_pending_too_large');
287
+ }
288
+ const handle = await openNoFollow(workItemPath, constants.O_RDONLY);
289
+ try {
290
+ const opened = await handle.stat({ bigint: true });
291
+ if (!sameStatIdentity(opened, snapshot)) {
292
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_pending_changed');
293
+ }
294
+ const raw = await handle.readFile({ encoding: 'utf8' });
295
+ const after = await handle.stat({ bigint: true });
296
+ if (!sameStatIdentity(after, snapshot)) {
297
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_pending_changed');
298
+ }
299
+ return parseWorkItem(JSON.parse(raw));
300
+ }
301
+ catch (error) {
302
+ if (error instanceof SelfUpdateFailureError)
303
+ throw error;
304
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_pending_invalid', {
305
+ cause: error,
306
+ });
307
+ }
308
+ finally {
309
+ await handle.close();
310
+ }
311
+ }
312
+ function parseWorkItem(value) {
313
+ if (!isRecord(value) || value['schema_version'] !== WORK_ITEM_SCHEMA_VERSION)
314
+ throw new Error('schema_version');
315
+ const operation = value['operation'];
316
+ if (operation !== 'install' && operation !== 'rollback')
317
+ throw new Error('operation');
318
+ const parsed = {
319
+ schema_version: 1,
320
+ operation_id: requireString(value, 'operation_id'),
321
+ operation,
322
+ target_path: requireString(value, 'target_path'),
323
+ backup_path: requireString(value, 'backup_path'),
324
+ source_path: requireString(value, 'source_path'),
325
+ target_identity: parseIdentity(value['target_identity']),
326
+ backup_identity: parseIdentity(value['backup_identity']),
327
+ source_identity: parseIdentity(value['source_identity']),
328
+ helper_identity: parseIdentity(value['helper_identity']),
329
+ };
330
+ if (value['staged_path'] !== undefined)
331
+ parsed.staged_path = requireString(value, 'staged_path');
332
+ if (value['staged_identity'] !== undefined)
333
+ parsed.staged_identity = parseIdentity(value['staged_identity']);
334
+ return parsed;
335
+ }
336
+ function parseIdentity(value) {
337
+ if (!isRecord(value))
338
+ throw new Error('identity');
339
+ const identity = {
340
+ dev: requireString(value, 'dev'),
341
+ ino: requireString(value, 'ino'),
342
+ size: requireString(value, 'size'),
343
+ mtime_ns: requireString(value, 'mtime_ns'),
344
+ ctime_ns: requireString(value, 'ctime_ns'),
345
+ sha256: requireString(value, 'sha256'),
346
+ };
347
+ assertSha256(identity.sha256, 'identity_sha256');
348
+ return identity;
349
+ }
350
+ async function snapshotRegularFile(path, label, maxBytes) {
351
+ const cleanPath = assertAbsoluteCleanPath(path, label);
352
+ const before = await lstat(cleanPath, { bigint: true }).catch((error) => {
353
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_unreadable`, {
354
+ cause: error,
355
+ });
356
+ });
357
+ if (!before.isFile() || before.isSymbolicLink()) {
358
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_not_regular`);
359
+ }
360
+ if (maxBytes !== undefined && before.size > BigInt(maxBytes)) {
361
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_too_large`);
362
+ }
363
+ const handle = await openNoFollow(cleanPath, constants.O_RDONLY);
364
+ try {
365
+ const opened = await handle.stat({ bigint: true });
366
+ assertStatIdentity(before, opened, `${label}_opened`);
367
+ const digest = await hashHandle(handle, maxBytes);
368
+ const after = await handle.stat({ bigint: true });
369
+ assertStatIdentity(opened, after, `${label}_changed`);
370
+ return identityFromStat(after, digest);
371
+ }
372
+ finally {
373
+ await handle.close();
374
+ }
375
+ }
376
+ async function assertSnapshot(path, expected, label) {
377
+ const actual = await snapshotRegularFile(path, label);
378
+ if (!sameIdentity(actual, expected)) {
379
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_changed`);
380
+ }
381
+ }
382
+ async function copyRegularFileExclusive(sourcePath, destinationPath, expectedSource, mode) {
383
+ const source = await openNoFollow(sourcePath, constants.O_RDONLY);
384
+ let destination;
385
+ let operationError;
386
+ try {
387
+ const openedSource = await source.stat({ bigint: true });
388
+ if (!sameStatIdentity(openedSource, expectedSource)) {
389
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_source_changed');
390
+ }
391
+ destination = await open(destinationPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, mode);
392
+ const hash = createHash('sha256');
393
+ const buffer = Buffer.allocUnsafe(COPY_BUFFER_BYTES);
394
+ let position = 0;
395
+ while (true) {
396
+ const { bytesRead } = await source.read(buffer, 0, buffer.byteLength, position);
397
+ if (bytesRead === 0)
398
+ break;
399
+ const chunk = buffer.subarray(0, bytesRead);
400
+ hash.update(chunk);
401
+ await writeFully(destination, chunk, position);
402
+ position += bytesRead;
403
+ }
404
+ if (!safeDigestEqual(hash.digest('hex'), expectedSource.sha256)) {
405
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_source_digest_changed');
406
+ }
407
+ const afterSource = await source.stat({ bigint: true });
408
+ if (!sameStatIdentity(afterSource, expectedSource)) {
409
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_source_changed');
410
+ }
411
+ await destination.sync();
412
+ }
413
+ catch (error) {
414
+ operationError = error;
415
+ }
416
+ finally {
417
+ try {
418
+ if (destination)
419
+ await destination.close();
420
+ }
421
+ finally {
422
+ await source.close();
423
+ }
424
+ }
425
+ if (operationError !== undefined) {
426
+ await removeBestEffort(destinationPath);
427
+ throw operationError;
428
+ }
429
+ }
430
+ async function writeFully(handle, bytes, startPosition) {
431
+ let offset = 0;
432
+ while (offset < bytes.byteLength) {
433
+ const { bytesWritten } = await handle.write(bytes, offset, bytes.byteLength - offset, startPosition + offset);
434
+ if (bytesWritten <= 0) {
435
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.COPY_FAILED, 'windows_updater_short_write');
436
+ }
437
+ offset += bytesWritten;
438
+ }
439
+ }
440
+ async function hashHandle(handle, maxBytes) {
441
+ const hash = createHash('sha256');
442
+ const buffer = Buffer.allocUnsafe(COPY_BUFFER_BYTES);
443
+ let position = 0;
444
+ while (true) {
445
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
446
+ if (bytesRead === 0)
447
+ break;
448
+ position += bytesRead;
449
+ if (maxBytes !== undefined && position > maxBytes) {
450
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_file_too_large');
451
+ }
452
+ hash.update(buffer.subarray(0, bytesRead));
453
+ }
454
+ return hash.digest('hex');
455
+ }
456
+ async function openNoFollow(path, flags) {
457
+ const noFollow = constants.O_NOFOLLOW ?? 0;
458
+ try {
459
+ return await open(path, flags | noFollow);
460
+ }
461
+ catch (error) {
462
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_open_failed', {
463
+ cause: error,
464
+ });
465
+ }
466
+ }
467
+ async function assertPrivateDirectory(path, label, create) {
468
+ const cleanPath = assertAbsoluteCleanPath(path, label);
469
+ if (create)
470
+ await mkdir(cleanPath, { recursive: true, mode: 0o700 });
471
+ await assertNoSymlinkedParent(cleanPath, label);
472
+ const before = await lstat(cleanPath, { bigint: true }).catch((error) => {
473
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_unreadable`, {
474
+ cause: error,
475
+ });
476
+ });
477
+ if (!before.isDirectory() || before.isSymbolicLink()) {
478
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_not_directory`);
479
+ }
480
+ const canonicalPath = await canonicalPathFor(cleanPath, label);
481
+ await assertNoSymlinkedParent(cleanPath, label);
482
+ await assertNoSymlinkedParent(canonicalPath, label);
483
+ const canonical = await lstat(canonicalPath, { bigint: true }).catch((error) => {
484
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_unreadable`, {
485
+ cause: error,
486
+ });
487
+ });
488
+ if (!canonical.isDirectory() || canonical.isSymbolicLink()) {
489
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_not_directory`);
490
+ }
491
+ assertDirectoryIdentity(before, canonical, `${label}_canonical_changed`);
492
+ return canonicalPath;
493
+ }
494
+ async function assertManagedRegularPath(path, stateDir, label) {
495
+ const canonicalPath = await assertCanonicalRegularPath(path, label);
496
+ const rel = relative(stateDir, canonicalPath);
497
+ if (!rel || rel === '..' || rel.startsWith(`..${separator()}`) || isAbsolute(rel)) {
498
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_outside_state_dir`);
499
+ }
500
+ return canonicalPath;
501
+ }
502
+ async function assertExternalRegularPath(path, stateDir, label) {
503
+ const canonicalPath = await assertCanonicalRegularPath(path, label);
504
+ const rel = relative(stateDir, canonicalPath);
505
+ if (!rel || (rel !== '..' && !rel.startsWith(`..${separator()}`) && !isAbsolute(rel))) {
506
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_inside_state_dir`);
507
+ }
508
+ return canonicalPath;
509
+ }
510
+ async function assertCanonicalRegularPath(path, label) {
511
+ const cleanPath = assertAbsoluteCleanPath(path, label);
512
+ await assertNoSymlinkedParent(cleanPath, label);
513
+ const identity = await snapshotRegularFile(cleanPath, label);
514
+ const canonicalPath = await canonicalPathFor(cleanPath, label);
515
+ await assertNoSymlinkedParent(cleanPath, label);
516
+ await assertNoSymlinkedParent(canonicalPath, label);
517
+ await assertSnapshot(canonicalPath, identity, `${label}_canonical`);
518
+ return canonicalPath;
519
+ }
520
+ async function canonicalPathFor(path, label) {
521
+ return realpath(path).catch((error) => {
522
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_unreadable`, {
523
+ cause: error,
524
+ });
525
+ });
526
+ }
527
+ async function assertNoSymlinkedParent(path, label) {
528
+ const parent = dirname(path);
529
+ const root = symlinkTraversalRoot(parent);
530
+ const directories = [];
531
+ for (let current = parent; !samePath(current, root); current = dirname(current)) {
532
+ directories.push(current);
533
+ }
534
+ for (const directory of directories.reverse()) {
535
+ const info = await lstat(directory).catch((error) => {
536
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_parent_unreadable`, { cause: error });
537
+ });
538
+ if (info.isSymbolicLink()) {
539
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_parent_symlinked`);
540
+ }
541
+ }
542
+ }
543
+ function symlinkTraversalRoot(path) {
544
+ const parsedRoot = parse(path).root;
545
+ if (process.platform !== 'win32')
546
+ return parsedRoot;
547
+ // node:path treats only `\\?\UNC\` as the root of an extended UNC path.
548
+ // Stop at the actual share root so we never probe non-filesystem server segments.
549
+ const extendedUncRoot = /^(\\\\\?\\UNC\\[^\\]+\\[^\\]+\\?)/i.exec(path)?.[1];
550
+ return extendedUncRoot ?? parsedRoot;
551
+ }
552
+ function assertAbsoluteCleanPath(path, label) {
553
+ if (!path || path.includes('\0') || !isAbsolute(path)) {
554
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_path_invalid`);
555
+ }
556
+ const cleanPath = resolve(path);
557
+ if (!samePath(cleanPath, path)) {
558
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_path_not_normalized`);
559
+ }
560
+ return cleanPath;
561
+ }
562
+ function assertStatIdentity(left, right, label) {
563
+ if (left.dev !== right.dev
564
+ || left.ino !== right.ino
565
+ || left.size !== right.size
566
+ || left.mtimeNs !== right.mtimeNs
567
+ || left.ctimeNs !== right.ctimeNs) {
568
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}`);
569
+ }
570
+ }
571
+ function assertDirectoryIdentity(left, right, label) {
572
+ // Directory size and timestamps legitimately change when updater files are
573
+ // created or scanned. Device + inode identify the directory without turning
574
+ // those content changes into false path-swap failures.
575
+ if (left.dev !== right.dev || left.ino !== right.ino) {
576
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}`);
577
+ }
578
+ }
579
+ function sameStatIdentity(stat, identity) {
580
+ return stat.dev.toString() === identity.dev
581
+ && stat.ino.toString() === identity.ino
582
+ && stat.size.toString() === identity.size
583
+ && stat.mtimeNs.toString() === identity.mtime_ns
584
+ && stat.ctimeNs.toString() === identity.ctime_ns;
585
+ }
586
+ function identityFromStat(stat, digest) {
587
+ return {
588
+ dev: stat.dev.toString(),
589
+ ino: stat.ino.toString(),
590
+ size: stat.size.toString(),
591
+ mtime_ns: stat.mtimeNs.toString(),
592
+ ctime_ns: stat.ctimeNs.toString(),
593
+ sha256: digest,
594
+ };
595
+ }
596
+ function sameIdentity(left, right) {
597
+ return sameContent(left, right)
598
+ && left.dev === right.dev
599
+ && left.ino === right.ino
600
+ && left.mtime_ns === right.mtime_ns
601
+ && left.ctime_ns === right.ctime_ns;
602
+ }
603
+ function sameContent(left, right) {
604
+ return left.size === right.size && safeDigestEqual(left.sha256, right.sha256);
605
+ }
606
+ function safeDigestEqual(left, right) {
607
+ if (!/^[0-9a-f]{64}$/i.test(left) || !/^[0-9a-f]{64}$/i.test(right))
608
+ return false;
609
+ return timingSafeEqual(Buffer.from(left.toLowerCase(), 'hex'), Buffer.from(right.toLowerCase(), 'hex'));
610
+ }
611
+ function assertSha256(value, label) {
612
+ if (!/^[0-9a-f]{64}$/i.test(value)) {
613
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, `windows_updater_${label}_invalid`);
614
+ }
615
+ }
616
+ async function writeJsonNoReplaceAtomic(path, value, mode) {
617
+ const tempPath = `${path}.${randomBytes(8).toString('hex')}.tmp`;
618
+ try {
619
+ await writeJsonExclusive(tempPath, value, mode);
620
+ await link(tempPath, path);
621
+ }
622
+ catch (error) {
623
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_pending_publish_failed', {
624
+ cause: error,
625
+ });
626
+ }
627
+ finally {
628
+ await removeBestEffort(tempPath);
629
+ }
630
+ }
631
+ async function writeJsonExclusive(path, value, mode) {
632
+ const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, mode);
633
+ try {
634
+ await handle.writeFile(`${JSON.stringify(value)}\n`, 'utf8');
635
+ await handle.sync();
636
+ }
637
+ finally {
638
+ await handle.close();
639
+ }
640
+ }
641
+ async function writeJsonAtomic(path, value, mode) {
642
+ const tempPath = `${path}.${randomBytes(8).toString('hex')}.tmp`;
643
+ try {
644
+ await writeJsonExclusive(tempPath, value, mode);
645
+ await rename(tempPath, path);
646
+ }
647
+ catch (error) {
648
+ await removeBestEffort(tempPath);
649
+ throw error;
650
+ }
651
+ }
652
+ async function assertPathAbsent(path, label) {
653
+ try {
654
+ await lstat(path);
655
+ }
656
+ catch (error) {
657
+ if (isErrno(error, 'ENOENT'))
658
+ return;
659
+ throw error;
660
+ }
661
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.RECOVERY_REQUIRED, `windows_updater_${label}_exists`);
662
+ }
663
+ async function removeKnownReplacementTemp(path) {
664
+ try {
665
+ const info = await lstat(path);
666
+ if (!info.isFile() || info.isSymbolicLink()) {
667
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_replacement_temp_unsafe');
668
+ }
669
+ await rm(path);
670
+ }
671
+ catch (error) {
672
+ if (!isErrno(error, 'ENOENT'))
673
+ throw error;
674
+ }
675
+ }
676
+ function updaterFailureCode(error) {
677
+ if (error instanceof SelfUpdateFailureError)
678
+ return error.failureCode;
679
+ return SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED;
680
+ }
681
+ function samePath(left, right) {
682
+ const normalizedLeft = resolve(left);
683
+ const normalizedRight = resolve(right);
684
+ return process.platform === 'win32'
685
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
686
+ : normalizedLeft === normalizedRight;
687
+ }
688
+ function separator() {
689
+ return process.platform === 'win32' ? '\\' : '/';
690
+ }
691
+ function assertWindowsPlatform(platform) {
692
+ if (platform !== 'win32') {
693
+ throw selfUpdateFailure(SELF_UPDATE_FAILURE_CODES.REPLACE_FAILED, 'windows_updater_platform_required');
694
+ }
695
+ }
696
+ async function removeBestEffort(path) {
697
+ try {
698
+ await rm(path, { force: true });
699
+ }
700
+ catch {
701
+ // Cleanup failure must not replace the primary updater error.
702
+ }
703
+ }
704
+ function requireString(value, key) {
705
+ const field = value[key];
706
+ if (typeof field !== 'string' || !field)
707
+ throw new Error(key);
708
+ return field;
709
+ }
710
+ function isRecord(value) {
711
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
712
+ }
713
+ function isErrno(error, code) {
714
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
715
+ }