@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.
Files changed (53) hide show
  1. package/dist/bin/evolver-proxy.d.ts +12 -0
  2. package/dist/bin/evolver-proxy.js +192 -59
  3. package/dist/daemon/proxyDaemon.js +10 -0
  4. package/dist/daemon/systemdNotifier.d.ts +2 -0
  5. package/dist/daemon/systemdNotifier.js +11 -1
  6. package/dist/lifecycle/claimNudge.js +1 -1
  7. package/dist/lifecycle/deployGuard.js +1 -1
  8. package/dist/lifecycle/legacyNodeId.js +1 -1
  9. package/dist/lifecycle/manager.js +1 -1
  10. package/dist/llm/bodyCapture.js +1 -1
  11. package/dist/llm/index.js +1 -1
  12. package/dist/llm/server.js +1 -1
  13. package/dist/llm/traceBackfill.js +1 -1
  14. package/dist/llm/traceConfig.js +1 -1
  15. package/dist/llm/traceControl.js +1 -1
  16. package/dist/llm/traceEnvelope.js +1 -1
  17. package/dist/llm/traceSink.js +1 -1
  18. package/dist/llm/traceUploadPayload.js +1 -1
  19. package/dist/llm/upstream.js +1 -1
  20. package/dist/router/cachePassthrough.js +1 -1
  21. package/dist/router/features.js +1 -1
  22. package/dist/router/index.js +1 -1
  23. package/dist/router/messagesRoute.js +1 -1
  24. package/dist/router/modelRouter.js +1 -1
  25. package/dist/router/providerRoutes.js +1 -1
  26. package/dist/router/sseScan.js +1 -1
  27. package/dist/selfUpdate/bootstrap.d.ts +106 -13
  28. package/dist/selfUpdate/bootstrap.js +3418 -176
  29. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  30. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  31. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  32. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  33. package/dist/selfUpdate/executor.d.ts +17 -6
  34. package/dist/selfUpdate/executor.js +158 -58
  35. package/dist/selfUpdate/index.d.ts +2 -1
  36. package/dist/selfUpdate/index.js +2 -1
  37. package/dist/selfUpdate/migration.d.ts +80 -15
  38. package/dist/selfUpdate/migration.js +2513 -156
  39. package/dist/selfUpdate/policy.js +2 -8
  40. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  41. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  42. package/dist/selfUpdate/releaseBinary.d.ts +3 -0
  43. package/dist/selfUpdate/releaseBinary.js +46 -3
  44. package/dist/selfUpdate/transaction.d.ts +8 -0
  45. package/dist/selfUpdate/transaction.js +166 -18
  46. package/dist/selfUpdate/unixController.d.ts +8 -0
  47. package/dist/selfUpdate/unixController.js +364 -38
  48. package/dist/selfUpdate/windowsController.d.ts +14 -2
  49. package/dist/selfUpdate/windowsController.js +482 -103
  50. package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
  51. package/dist/selfUpdate/windowsUpdater.js +174 -7
  52. package/dist/sync/engine.js +1 -1
  53. package/package.json +3 -3
@@ -9,40 +9,47 @@
9
9
  // RESOLVE (version / asset name / dest path)
10
10
  // DOWNLOAD (signed manifest + binary staged under tmpdir)
11
11
  // VERIFY (ed25519 manifest signature + sha256 + real preflight probe)
12
- // INSTALL (atomic copy into <home>/bin; unverified bytes are never written)
13
- // REGISTER (spawn `evolver lifecycle bootstrap` with EVOLVER_SELF_UPDATE_TARGET_PATH=dest)
12
+ // INSTALL (validate target ownership; publish verified bytes atomically without replacement)
13
+ // REGISTER (delegate to the lifecycle bootstrap transaction runner with absolute deadlines)
14
14
  //
15
15
  // Migration is a convenience, never an escalation: it is skipped when disabled
16
16
  // (EVOLVER_BOOTSTRAP_MIGRATION=0|off), for root (non-win32), CI, containers, within a
17
- // bootstrap-failure cooldown window, and on unsupported platforms. Every failure degrades
18
- // back to the existing 'off + warning' startup and records a bootstrap attempt marker so
19
- // the cooldown applies. All I/O is injectable for tests.
20
- import { spawn } from 'node:child_process';
21
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
22
- import { chmod as fsChmod, lstat, mkdir as fsMkdir, readFile as fsReadFile, rename as fsRename, rm as fsRm, writeFile as fsWriteFile, } from 'node:fs/promises';
17
+ // bootstrap-failure cooldown window, and on unsupported platforms. Clean failures degrade
18
+ // to the existing 'off + warning' startup; ambiguous child/process-tree ownership requires
19
+ // foreground exit. Clean failures use cooldown; ambiguous ownership writes a durable blocker
20
+ // that keeps later foreground startups fail-closed until signed lifecycle reconciliation
21
+ // proves a committed or cleanly rolled-back terminal outcome.
22
+ import { execFileSync } from 'node:child_process';
23
+ import { createHash, randomUUID } from 'node:crypto';
24
+ import { closeSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
25
+ import { chmod as fsChmod, link as fsLink, lstat, mkdir as fsMkdir, open as fsOpen, readFile as fsReadFile, realpath as fsRealpath, rename as fsRename, rm as fsRm, rmdir as fsRmdir, unlink as fsUnlink, writeFile as fsWriteFile, } from 'node:fs/promises';
23
26
  import { homedir } from 'node:os';
24
- import { dirname, join } from 'node:path';
25
- import { ops } from '@evomap/evolver-core';
27
+ import { dirname, isAbsolute, join, parse as parsePath, relative, resolve as resolvePath, win32 } from 'node:path';
28
+ import { ops, util } from '@evomap/evolver-core';
26
29
  import { SELF_UPDATE_FAILURE_CODES } from './failureCodes.js';
27
30
  import { resolveSelfUpdatePublicKey } from './builtinKey.js';
28
31
  import { downloadGithubReleaseArtifact, releaseAssetName, resolveGithubReleaseManifest, } from './releaseBinary.js';
29
32
  import { preflightManagedStagedBinary } from './transaction.js';
30
33
  import { getCurrentVersion } from './version.js';
31
- import { looksLikeContainer, recentBootstrapFailure, recordBootstrapAttempt, resolveBootstrapCliInvocation, resolveBootstrapStateDir, } from './bootstrap.js';
32
- /** Advisory migration state marker written next to the bootstrap attempt marker. */
33
- export const MIGRATION_STATE_FILE = 'migration.json';
34
- /**
35
- * The CLI-side service activation itself spawns up to ~60s (lifecycle powershell/schtasks
36
- * activation), so a shorter timeout would kill the child mid-activation and leave the
37
- * installed binary registered as failed. Mirrors the bootstrap timeout rationale.
38
- */
39
- export const MIGRATION_REGISTER_TIMEOUT_MS = 90_000;
34
+ import { looksLikeContainer, recentBootstrapFailure, recordBootstrapAttempt, resolveBootstrapStateDir, } from './bootstrap.js';
35
+ /** Canonical migration transaction state written next to the bootstrap attempt marker. */
36
+ const MIGRATION_STATE_FILE = 'migration.json';
37
+ // Keep the child transaction deadline shorter than its parent observation deadline so the
38
+ // child can durably roll back before process-tree containment becomes necessary.
39
+ const MIGRATION_REGISTER_TRANSACTION_BUDGET_MS = 180_000;
40
+ const MIGRATION_REGISTER_TIMEOUT_MS = 210_000;
41
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
42
+ const MAX_MIGRATION_DETAIL_LENGTH = 512;
43
+ const MAX_MIGRATION_STATE_BYTES = 16 * 1024;
44
+ const MIGRATION_LOCK_FILE = '.evolver-standalone-migration.lock';
45
+ const BOOTSTRAP_ATTEMPT_FILE = 'bootstrap-attempt.json';
46
+ const HOST_WINDOWS_SYSTEM_ROOT = process.env['SystemRoot']?.trim() || 'C:\\Windows';
40
47
  /**
41
48
  * Migration install home — mirrors the CLI-side lifecyclePaths home resolution
42
49
  * (kept dependency-free across packages): EVOLVER_HOME ?? EVOMAP_HOME ?? ~/.evomap.
43
50
  */
44
51
  export function resolveMigrationHome(env) {
45
- return env['EVOLVER_HOME'] ?? env['EVOMAP_HOME'] ?? join(homedir(), '.evomap');
52
+ return resolvePath(env['EVOLVER_HOME'] ?? env['EVOMAP_HOME'] ?? join(homedir(), '.evomap'));
46
53
  }
47
54
  /** Migration install path for this platform: <home>/bin/<releaseAssetName>. Throws on unsupported platforms. */
48
55
  export function resolveMigrationDestPath(env, platform = process.platform, arch = process.arch) {
@@ -60,23 +67,480 @@ export function resolveMigrationVersion(env) {
60
67
  return ops.normalizeConcreteVersion(getCurrentVersion());
61
68
  }
62
69
  const defaultReadTextFile = (path) => readFileSync(path, 'utf8');
70
+ function assertTrustedMigrationStateLeafSync(path, options = {}) {
71
+ const existing = statMigrationStateLeafSync(path);
72
+ if (options.assertStateFileTrust) {
73
+ options.assertStateFileTrust(path);
74
+ }
75
+ else if (process.platform === 'win32') {
76
+ assertWindowsMigrationAclTrusted([{ path, parentOnly: false }]);
77
+ }
78
+ else {
79
+ const uid = resolveProcessUid();
80
+ if ((uid !== undefined && existing.uid !== BigInt(uid)) || (existing.mode & 18n) !== 0n) {
81
+ throw new Error('untrusted_migration_state_leaf');
82
+ }
83
+ }
84
+ return existing;
85
+ }
86
+ function statMigrationStateLeafSync(path) {
87
+ const existing = lstatSync(path, { bigint: true });
88
+ if (existing.isSymbolicLink() || !existing.isFile() || existing.nlink !== 1n) {
89
+ throw new Error('unsafe_migration_state_leaf');
90
+ }
91
+ return existing;
92
+ }
93
+ function sameMigrationStateFileIdentity(left, right) {
94
+ return left.dev === right.dev
95
+ && left.ino === right.ino
96
+ && left.nlink === 1n
97
+ && right.nlink === 1n;
98
+ }
99
+ function sameMigrationStateFileSnapshot(left, right) {
100
+ return sameMigrationStateFileIdentity(left, right)
101
+ && left.size === right.size
102
+ && left.mtimeNs === right.mtimeNs
103
+ && left.ctimeNs === right.ctimeNs
104
+ && left.mode === right.mode
105
+ && left.uid === right.uid
106
+ && left.gid === right.gid;
107
+ }
108
+ function readBoundedMigrationStateFd(fd) {
109
+ const bytes = Buffer.allocUnsafe(MAX_MIGRATION_STATE_BYTES + 1);
110
+ let offset = 0;
111
+ while (offset < bytes.byteLength) {
112
+ const read = readSync(fd, bytes, offset, bytes.byteLength - offset, offset);
113
+ if (read === 0)
114
+ break;
115
+ offset += read;
116
+ }
117
+ if (offset > MAX_MIGRATION_STATE_BYTES)
118
+ throw new Error('migration_state_too_large');
119
+ return bytes.subarray(0, offset);
120
+ }
121
+ function readTrustedMigrationStateText(path, options = {}) {
122
+ let initial;
123
+ try {
124
+ initial = assertTrustedMigrationStateLeafSync(path, options);
125
+ }
126
+ catch (error) {
127
+ if (isErrno(error, 'ENOENT'))
128
+ return undefined;
129
+ throw error;
130
+ }
131
+ if (initial.size > BigInt(MAX_MIGRATION_STATE_BYTES)) {
132
+ throw new Error('migration_state_too_large');
133
+ }
134
+ const directory = dirname(resolvePath(path));
135
+ if (options.assertStateDirectoryTrust) {
136
+ options.assertStateDirectoryTrust(directory);
137
+ }
138
+ else {
139
+ assertTrustedMigrationDirectoryChainSync(directory);
140
+ }
141
+ const beforeOpen = statMigrationStateLeafSync(path);
142
+ if (!sameMigrationStateFileSnapshot(initial, beforeOpen)) {
143
+ throw new Error('migration_state_leaf_changed');
144
+ }
145
+ const fd = openSync(path, 'r');
146
+ try {
147
+ const opened = fstatSync(fd, { bigint: true });
148
+ if (!sameMigrationStateFileSnapshot(beforeOpen, opened)
149
+ || opened.size > BigInt(MAX_MIGRATION_STATE_BYTES)) {
150
+ throw new Error('migration_state_leaf_changed');
151
+ }
152
+ const raw = readBoundedMigrationStateFd(fd);
153
+ const afterRead = statMigrationStateLeafSync(path);
154
+ const afterFdRead = fstatSync(fd, { bigint: true });
155
+ const finalPath = assertTrustedMigrationStateLeafSync(path, options);
156
+ const finalFd = fstatSync(fd, { bigint: true });
157
+ const confirmedRaw = readBoundedMigrationStateFd(fd);
158
+ const confirmedPath = statMigrationStateLeafSync(path);
159
+ const confirmedFd = fstatSync(fd, { bigint: true });
160
+ if (!sameMigrationStateFileSnapshot(opened, afterRead)
161
+ || !sameMigrationStateFileSnapshot(opened, afterFdRead)
162
+ || !sameMigrationStateFileSnapshot(opened, finalPath)
163
+ || !sameMigrationStateFileSnapshot(opened, finalFd)
164
+ || !sameMigrationStateFileSnapshot(opened, confirmedPath)
165
+ || !sameMigrationStateFileSnapshot(opened, confirmedFd)
166
+ || finalFd.size !== BigInt(raw.byteLength)
167
+ || confirmedFd.size !== BigInt(confirmedRaw.byteLength)
168
+ || !raw.equals(confirmedRaw)) {
169
+ throw new Error('migration_state_leaf_changed');
170
+ }
171
+ return raw.toString('utf8');
172
+ }
173
+ finally {
174
+ closeSync(fd);
175
+ }
176
+ }
63
177
  const defaultWriteTextFile = (path, content) => {
64
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
65
- writeFileSync(path, content, { encoding: 'utf8', mode: 0o600 });
178
+ const directory = dirname(resolvePath(path));
179
+ assertTrustedMigrationDirectoryChainSync(directory);
180
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
181
+ assertTrustedMigrationDirectoryChainSync(directory);
182
+ if (migrationPathKey(realpathSync(directory)) !== migrationPathKey(directory)) {
183
+ throw new Error('unsafe_migration_state_directory');
184
+ }
185
+ try {
186
+ assertTrustedMigrationStateLeafSync(path);
187
+ }
188
+ catch (error) {
189
+ if (!isErrno(error, 'ENOENT'))
190
+ throw error;
191
+ }
192
+ const temporaryPath = join(directory, `.${parsePath(path).base}.${process.pid}.${randomUUID()}.tmp`);
193
+ let fd;
194
+ try {
195
+ fd = openSync(temporaryPath, 'wx', 0o600);
196
+ writeFileSync(fd, content, { encoding: 'utf8' });
197
+ fsyncSync(fd);
198
+ closeSync(fd);
199
+ fd = undefined;
200
+ renameSync(temporaryPath, path);
201
+ const published = lstatSync(path, { bigint: true });
202
+ if (published.isSymbolicLink() || !published.isFile() || published.nlink !== 1n) {
203
+ throw new Error('unsafe_migration_state_publication');
204
+ }
205
+ try {
206
+ const directoryFd = openSync(directory, 'r');
207
+ try {
208
+ fsyncSync(directoryFd);
209
+ }
210
+ finally {
211
+ closeSync(directoryFd);
212
+ }
213
+ }
214
+ catch (error) {
215
+ // Windows does not consistently allow directory handles; POSIX durability requires it.
216
+ if (process.platform !== 'win32')
217
+ throw error;
218
+ }
219
+ }
220
+ finally {
221
+ if (fd !== undefined) {
222
+ try {
223
+ closeSync(fd);
224
+ }
225
+ catch {
226
+ // Advisory state publication has already failed; cleanup is best effort.
227
+ }
228
+ }
229
+ try {
230
+ unlinkSync(temporaryPath);
231
+ }
232
+ catch {
233
+ // Atomic temp names are owner-only inside a trusted directory; retry on next state write.
234
+ }
235
+ }
66
236
  };
67
237
  function resolveProcessUid() {
68
238
  const getuid = process.getuid;
69
239
  return typeof getuid === 'function' ? getuid.call(process) : undefined;
70
240
  }
71
- /** Best-effort advisory migration state log; never throws — bookkeeping must not break startup. */
241
+ function sameMigrationInstallAnchor(left, right) {
242
+ return left.phase === right.phase
243
+ && left.temporaryPath === right.temporaryPath
244
+ && left.size === right.size
245
+ && left.sha256 === right.sha256
246
+ && left.directoryCreated === right.directoryCreated
247
+ && (left.phase === 'planned'
248
+ || (right.phase !== 'planned'
249
+ && left.device === right.device
250
+ && left.inode === right.inode));
251
+ }
252
+ const MIGRATION_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
253
+ const MIGRATION_DIGEST_RE = /^[0-9a-f]{64}$/;
254
+ const MIGRATION_UNSIGNED_INTEGER_RE = /^[0-9]+$/;
255
+ const MIGRATION_POSITIVE_INTEGER_RE = /^[1-9][0-9]*$/;
256
+ const MIGRATION_SIGNED_INTEGER_RE = /^-?[0-9]+$/;
257
+ function exactMigrationRecordKeys(record, expected) {
258
+ const actual = Object.keys(record).sort();
259
+ const sortedExpected = [...expected].sort();
260
+ return actual.length === sortedExpected.length
261
+ && actual.every((key, index) => key === sortedExpected[index]);
262
+ }
263
+ function exactMigrationTimestamp(value) {
264
+ if (typeof value !== 'string')
265
+ return false;
266
+ const parsed = Date.parse(value);
267
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
268
+ }
269
+ function boundedMigrationStateText(value, maxLength) {
270
+ return typeof value === 'string'
271
+ && value.length > 0
272
+ && value.length <= maxLength
273
+ && !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value);
274
+ }
275
+ function parseMigrationInstallAnchor(value, destPath) {
276
+ if (!value || typeof value !== 'object' || Array.isArray(value))
277
+ return undefined;
278
+ const anchor = value;
279
+ const phase = anchor['phase'];
280
+ const expectedKeys = phase === 'planned'
281
+ ? ['phase', 'temporaryPath', 'size', 'sha256', 'directoryCreated']
282
+ : ['phase', 'temporaryPath', 'device', 'inode', 'size', 'sha256', 'directoryCreated'];
283
+ if (!exactMigrationRecordKeys(anchor, expectedKeys))
284
+ return undefined;
285
+ const temporaryPath = anchor['temporaryPath'];
286
+ const device = anchor['device'];
287
+ const inode = anchor['inode'];
288
+ const size = anchor['size'];
289
+ const digest = anchor['sha256'];
290
+ const directoryCreated = anchor['directoryCreated'];
291
+ if (typeof temporaryPath !== 'string'
292
+ || !isAbsolute(temporaryPath)
293
+ || temporaryPath.length > 4_096
294
+ || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(temporaryPath)
295
+ || dirname(temporaryPath) !== dirname(destPath)
296
+ || !parsePath(temporaryPath).base.startsWith(`.${parsePath(destPath).base}.`)
297
+ || !parsePath(temporaryPath).base.endsWith('.migration')
298
+ || typeof size !== 'number'
299
+ || !Number.isSafeInteger(size)
300
+ || size < 0
301
+ || typeof digest !== 'string'
302
+ || !MIGRATION_DIGEST_RE.test(digest)
303
+ || typeof directoryCreated !== 'boolean') {
304
+ return undefined;
305
+ }
306
+ if (phase === 'planned') {
307
+ return { phase, temporaryPath, size, sha256: digest, directoryCreated };
308
+ }
309
+ if ((phase !== 'materialized' && phase !== 'cleanup')
310
+ || typeof device !== 'string'
311
+ || !MIGRATION_POSITIVE_INTEGER_RE.test(device)
312
+ || typeof inode !== 'string'
313
+ || !MIGRATION_POSITIVE_INTEGER_RE.test(inode)) {
314
+ return undefined;
315
+ }
316
+ return { phase, temporaryPath, device, inode, size, sha256: digest, directoryCreated };
317
+ }
318
+ function parseMigrationInstallOwnership(value) {
319
+ if (!value || typeof value !== 'object' || Array.isArray(value))
320
+ return undefined;
321
+ const ownership = value;
322
+ if (!exactMigrationRecordKeys(ownership, ['disposition', 'preimage', 'directoryCreated'])
323
+ || (ownership['disposition'] !== 'installed' && ownership['disposition'] !== 'converged')
324
+ || (ownership['preimage'] !== 'absent' && ownership['preimage'] !== 'matching_target')
325
+ || typeof ownership['directoryCreated'] !== 'boolean'
326
+ || (ownership['disposition'] === 'installed' && ownership['preimage'] !== 'absent')
327
+ || (ownership['disposition'] === 'converged'
328
+ && (ownership['preimage'] !== 'matching_target' || ownership['directoryCreated']))) {
329
+ return undefined;
330
+ }
331
+ return {
332
+ disposition: ownership['disposition'],
333
+ preimage: ownership['preimage'],
334
+ directoryCreated: ownership['directoryCreated'],
335
+ };
336
+ }
337
+ function parseMigrationTargetIdentity(value, destPath) {
338
+ if (!value || typeof value !== 'object' || Array.isArray(value))
339
+ return undefined;
340
+ const identity = value;
341
+ if (!exactMigrationRecordKeys(identity, ['path', 'device', 'inode', 'size', 'linkCount', 'mtimeNs', 'ctimeNs', 'sha256'])) {
342
+ return undefined;
343
+ }
344
+ const path = identity['path'];
345
+ const device = identity['device'];
346
+ const inode = identity['inode'];
347
+ const size = identity['size'];
348
+ const linkCount = identity['linkCount'];
349
+ const mtimeNs = identity['mtimeNs'];
350
+ const ctimeNs = identity['ctimeNs'];
351
+ const digest = identity['sha256'];
352
+ if (path !== destPath
353
+ || typeof device !== 'string'
354
+ || !MIGRATION_UNSIGNED_INTEGER_RE.test(device)
355
+ || typeof inode !== 'string'
356
+ || !MIGRATION_UNSIGNED_INTEGER_RE.test(inode)
357
+ || typeof size !== 'number'
358
+ || !Number.isSafeInteger(size)
359
+ || size < 0
360
+ || linkCount !== 1
361
+ || typeof mtimeNs !== 'string'
362
+ || !MIGRATION_SIGNED_INTEGER_RE.test(mtimeNs)
363
+ || typeof ctimeNs !== 'string'
364
+ || !MIGRATION_SIGNED_INTEGER_RE.test(ctimeNs)
365
+ || typeof digest !== 'string'
366
+ || !MIGRATION_DIGEST_RE.test(digest)) {
367
+ return undefined;
368
+ }
369
+ return {
370
+ path: destPath,
371
+ device,
372
+ inode,
373
+ size,
374
+ linkCount: 1,
375
+ mtimeNs,
376
+ ctimeNs,
377
+ sha256: digest,
378
+ };
379
+ }
380
+ function parseCanonicalMigrationState(raw) {
381
+ if (raw.length === 0 || Buffer.byteLength(raw, 'utf8') > MAX_MIGRATION_STATE_BYTES) {
382
+ return undefined;
383
+ }
384
+ let parsed;
385
+ try {
386
+ parsed = JSON.parse(raw);
387
+ }
388
+ catch {
389
+ return undefined;
390
+ }
391
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
392
+ return undefined;
393
+ const record = parsed;
394
+ const version = record['version'];
395
+ const destPath = record['destPath'];
396
+ const attemptedAt = record['attemptedAt'];
397
+ if (record['schemaVersion'] !== 1
398
+ || typeof version !== 'string'
399
+ || version.length > 128
400
+ || ops.normalizeConcreteVersion(version) !== version
401
+ || typeof destPath !== 'string'
402
+ || !isAbsolute(destPath)
403
+ || destPath.length > 4_096
404
+ || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(destPath)
405
+ || !exactMigrationTimestamp(attemptedAt)) {
406
+ return undefined;
407
+ }
408
+ const base = { schemaVersion: 1, version, destPath, attemptedAt };
409
+ const baseKeys = ['schemaVersion', 'state', 'version', 'destPath', 'attemptedAt'];
410
+ if (record['state'] === 'in_progress') {
411
+ if (record['reason'] === undefined) {
412
+ return exactMigrationRecordKeys(record, baseKeys) ? { ...base, state: 'in_progress' } : undefined;
413
+ }
414
+ return record['reason'] === 'install_recovered'
415
+ && exactMigrationRecordKeys(record, [...baseKeys, 'reason'])
416
+ ? { ...base, state: 'in_progress', reason: 'install_recovered' }
417
+ : undefined;
418
+ }
419
+ if (record['state'] === 'failed') {
420
+ return exactMigrationRecordKeys(record, [...baseKeys, 'reason'])
421
+ && boundedMigrationStateText(record['reason'], 1_024)
422
+ ? { ...base, state: 'failed', reason: record['reason'] }
423
+ : undefined;
424
+ }
425
+ if (record['state'] === 'installing') {
426
+ const installAnchor = parseMigrationInstallAnchor(record['installAnchor'], destPath);
427
+ return installAnchor && exactMigrationRecordKeys(record, [...baseKeys, 'installAnchor'])
428
+ ? { ...base, state: 'installing', installAnchor }
429
+ : undefined;
430
+ }
431
+ const intentId = record['intentId'];
432
+ const targetIdentity = parseMigrationTargetIdentity(record['targetIdentity'], destPath);
433
+ const installOwnership = parseMigrationInstallOwnership(record['installOwnership']);
434
+ const hasRegistrationBase = typeof intentId === 'string'
435
+ && MIGRATION_UUID_RE.test(intentId)
436
+ && targetIdentity !== undefined
437
+ && installOwnership !== undefined;
438
+ if (record['state'] === 'registering' || record['state'] === 'committed') {
439
+ return hasRegistrationBase
440
+ && exactMigrationRecordKeys(record, [...baseKeys, 'intentId', 'targetIdentity', 'installOwnership'])
441
+ ? { ...base, state: record['state'], intentId, targetIdentity, installOwnership }
442
+ : undefined;
443
+ }
444
+ if (record['state'] === 'rollback_pending') {
445
+ return hasRegistrationBase
446
+ && boundedMigrationStateText(record['reason'], 1_024)
447
+ && exactMigrationRecordKeys(record, [...baseKeys, 'intentId', 'targetIdentity', 'installOwnership', 'reason'])
448
+ ? {
449
+ ...base,
450
+ state: 'rollback_pending',
451
+ intentId,
452
+ targetIdentity,
453
+ installOwnership,
454
+ reason: record['reason'],
455
+ }
456
+ : undefined;
457
+ }
458
+ if (record['state'] === 'blocked') {
459
+ return hasRegistrationBase
460
+ && boundedMigrationStateText(record['reason'], 1_024)
461
+ && exactMigrationRecordKeys(record, [...baseKeys, 'intentId', 'targetIdentity', 'installOwnership', 'reason'])
462
+ ? {
463
+ ...base,
464
+ state: 'blocked',
465
+ intentId,
466
+ targetIdentity,
467
+ installOwnership,
468
+ reason: record['reason'],
469
+ }
470
+ : undefined;
471
+ }
472
+ if (record['state'] !== 'rolled_back'
473
+ || !boundedMigrationStateText(record['reason'], 1_024)) {
474
+ return undefined;
475
+ }
476
+ const installAnchor = parseMigrationInstallAnchor(record['installAnchor'], destPath);
477
+ if (installAnchor
478
+ && exactMigrationRecordKeys(record, [...baseKeys, 'reason', 'installAnchor'])) {
479
+ return { ...base, state: 'rolled_back', reason: record['reason'], installAnchor };
480
+ }
481
+ return hasRegistrationBase
482
+ && exactMigrationRecordKeys(record, [...baseKeys, 'reason', 'intentId', 'targetIdentity', 'installOwnership'])
483
+ ? {
484
+ ...base,
485
+ state: 'rolled_back',
486
+ reason: record['reason'],
487
+ intentId,
488
+ targetIdentity,
489
+ installOwnership,
490
+ }
491
+ : undefined;
492
+ }
493
+ function inspectMigrationStartupState(env, options) {
494
+ let raw;
495
+ try {
496
+ raw = readTrustedMigrationStateText(join(resolveBootstrapStateDir(env), MIGRATION_STATE_FILE), options);
497
+ }
498
+ catch (error) {
499
+ return { classification: 'unsafe', canonicalPresent: true, detail: errorDetail(error) };
500
+ }
501
+ if (raw === undefined)
502
+ return { classification: 'none', canonicalPresent: false };
503
+ const record = parseCanonicalMigrationState(raw);
504
+ if (!record) {
505
+ return { classification: 'unsafe', canonicalPresent: true, detail: 'migration_state_invalid' };
506
+ }
507
+ if (record.state === 'rolled_back' || record.state === 'failed') {
508
+ return { classification: 'none', canonicalPresent: true };
509
+ }
510
+ return { classification: 'retryable', canonicalPresent: true, record, raw };
511
+ }
512
+ function serializeMigrationState(record, options) {
513
+ const payload = {
514
+ schemaVersion: 1,
515
+ ...record,
516
+ attemptedAt: new Date(options.now ?? Date.now()).toISOString(),
517
+ };
518
+ return `${JSON.stringify(payload)}\n`;
519
+ }
520
+ function persistAuthoritativeMigrationState(env, record, options) {
521
+ const payload = serializeMigrationState(record, options);
522
+ const path = join(resolveBootstrapStateDir(env), MIGRATION_STATE_FILE);
523
+ const write = options.writeFile ?? defaultWriteTextFile;
524
+ write(path, payload);
525
+ if (readTrustedMigrationStateText(path, options) !== payload) {
526
+ throw new Error('migration_state_durability_unconfirmed');
527
+ }
528
+ return payload;
529
+ }
530
+ function assertAuthoritativeMigrationStateCurrent(env, expectedPayload, options) {
531
+ if (readTrustedMigrationStateText(join(resolveBootstrapStateDir(env), MIGRATION_STATE_FILE), options)
532
+ !== expectedPayload) {
533
+ throw new Error('migration_registration_intent_changed');
534
+ }
535
+ }
72
536
  function writeMigrationState(env, record, options) {
73
537
  try {
74
- const payload = { ...record, attemptedAt: new Date(options.now ?? Date.now()).toISOString() };
538
+ const payload = serializeMigrationState(record, options);
75
539
  const write = options.writeFile ?? defaultWriteTextFile;
76
- write(join(resolveBootstrapStateDir(env), MIGRATION_STATE_FILE), `${JSON.stringify(payload)}\n`);
540
+ write(join(resolveBootstrapStateDir(env), MIGRATION_STATE_FILE), payload);
77
541
  }
78
542
  catch {
79
- // State log is advisory; startup continues regardless.
543
+ // Pre-registration diagnostics are advisory; registration intent is persisted separately.
80
544
  }
81
545
  }
82
546
  async function rmSafe(rm, path) {
@@ -88,66 +552,1470 @@ async function rmSafe(rm, path) {
88
552
  }
89
553
  }
90
554
  function errorDetail(err) {
91
- if (err instanceof Error)
92
- return err.message;
93
- return String(err);
94
- }
95
- /** Spawn `evolver lifecycle bootstrap` with the migration target bound; bounded timeout. */
96
- function registerMigratedBinary(env, destPath, options, exists) {
97
- const invocation = resolveBootstrapCliInvocation({ execPath: options.execPath, exists });
98
- if (!invocation)
99
- return Promise.resolve({ ok: false, reason: 'cli_not_found' });
100
- // resolveBootstrapCliInvocation already targets `lifecycle bootstrap`; normalize the tail
101
- // explicitly while preserving any CLI entry prefix (node <cli.js> ...).
102
- const args = [...invocation.args.slice(0, Math.max(0, invocation.args.length - 2)), 'lifecycle', 'bootstrap'];
103
- const spawnFn = options.spawnFn ?? spawn;
104
- const childEnv = { ...env, EVOLVER_SELF_UPDATE_TARGET_PATH: destPath };
105
- const timeoutMs = options.timeoutMs ?? MIGRATION_REGISTER_TIMEOUT_MS;
106
- return new Promise((resolvePromise) => {
107
- let child;
555
+ const raw = err instanceof Error ? err.message : String(err);
556
+ const normalized = raw
557
+ .replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu, ' ')
558
+ .replace(/\s+/gu, ' ')
559
+ .trim();
560
+ return normalized.slice(0, MAX_MIGRATION_DETAIL_LENGTH) || 'unknown';
561
+ }
562
+ function registrationReason(reason) {
563
+ const normalized = errorDetail(reason);
564
+ return /^[a-z][a-z0-9_]{0,63}$/.test(normalized) ? normalized : undefined;
565
+ }
566
+ function isErrno(error, code) {
567
+ return typeof error === 'object' && error !== null && error.code === code;
568
+ }
569
+ function migrationPathKey(path) {
570
+ const resolved = resolvePath(path);
571
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
572
+ }
573
+ function trustedWindowsSystemExecutable(name) {
574
+ if (!win32.isAbsolute(HOST_WINDOWS_SYSTEM_ROOT) || /[\r\n\0]/.test(HOST_WINDOWS_SYSTEM_ROOT)) {
575
+ throw new Error('Windows SystemRoot is not an absolute trusted path');
576
+ }
577
+ return win32.join(HOST_WINDOWS_SYSTEM_ROOT, 'System32', name);
578
+ }
579
+ function windowsMigrationAclScript(checks) {
580
+ const encodedChecks = Buffer.from(JSON.stringify(checks), 'utf8').toString('base64');
581
+ return [
582
+ `$ErrorActionPreference = 'Stop'`,
583
+ 'try {',
584
+ ` $json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedChecks}'))`,
585
+ ' $checks = $json | ConvertFrom-Json',
586
+ ...windowsMigrationAclRules().map((line) => ` ${line}`),
587
+ '} catch { exit 24 }',
588
+ ].join('; ');
589
+ }
590
+ function windowsMigrationAclRules() {
591
+ const d = String.fromCharCode(36);
592
+ return [
593
+ `${d}userSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value`,
594
+ `${d}trustedInstaller = 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464'`,
595
+ ...windowsMigrationAclPermissionRules(d),
596
+ ];
597
+ }
598
+ function windowsMigrationAclPermissionRules(d) {
599
+ return [
600
+ `${d}trustedOwners = @(${d}userSid, 'S-1-5-18', 'S-1-5-32-544', ${d}trustedInstaller)`,
601
+ `${d}trustedWriters = @(${d}userSid, 'S-1-5-18', 'S-1-5-32-544', ${d}trustedInstaller, 'S-1-3-0', 'S-1-3-4')`,
602
+ `${d}parentDanger = [System.Security.AccessControl.FileSystemRights]::Delete -bor [System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor [System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor [System.Security.AccessControl.FileSystemRights]::TakeOwnership`,
603
+ `${d}contentDanger = [System.Security.AccessControl.FileSystemRights]::WriteData -bor [System.Security.AccessControl.FileSystemRights]::AppendData -bor [System.Security.AccessControl.FileSystemRights]::CreateFiles -bor [System.Security.AccessControl.FileSystemRights]::CreateDirectories`,
604
+ ...windowsMigrationAclLoopRules(d),
605
+ ];
606
+ }
607
+ function windowsMigrationAclLoopRules(d) {
608
+ return [
609
+ `foreach (${d}check in @(${d}checks)) {`,
610
+ ` ${d}acl = Get-Acl -LiteralPath ${d}check.path`,
611
+ ` ${d}owner = ${d}acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value`,
612
+ ` if (${d}trustedOwners -notcontains ${d}owner) { exit 21 }`,
613
+ ` ${d}danger = if (${d}check.parentOnly) { ${d}parentDanger } else { ${d}parentDanger -bor ${d}contentDanger }`,
614
+ ` foreach (${d}rule in @(${d}acl.Access)) {`,
615
+ ` if (${d}rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { continue }`,
616
+ ` if ((${d}rule.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly) -ne 0) { continue }`,
617
+ ` if ((${d}rule.FileSystemRights -band ${d}danger) -eq 0) { continue }`,
618
+ ` try { ${d}sid = ${d}rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value } catch { exit 22 }`,
619
+ ` if (${d}trustedWriters -notcontains ${d}sid) { exit 23 }`,
620
+ ' }',
621
+ '}',
622
+ ];
623
+ }
624
+ function assertWindowsMigrationAclTrusted(checks) {
625
+ if (process.platform !== 'win32' || checks.length === 0)
626
+ return;
627
+ try {
628
+ execFileSync(trustedWindowsSystemExecutable('WindowsPowerShell\\v1.0\\powershell.exe'), ['-NoProfile', '-NonInteractive', '-Command', windowsMigrationAclScript(checks)], { stdio: 'ignore', timeout: 10_000, windowsHide: true });
629
+ }
630
+ catch (error) {
631
+ throw new Error('migration Windows ACL chain is not trusted', { cause: error });
632
+ }
633
+ }
634
+ function assertTrustedMigrationDirectoryChainSync(directory) {
635
+ let current = resolvePath(directory);
636
+ const root = parsePath(current).root;
637
+ const windowsChecks = [];
638
+ const uid = resolveProcessUid();
639
+ let nearestExisting = true;
640
+ let privateUserAnchor = false;
641
+ for (;;) {
642
+ let info;
643
+ try {
644
+ info = lstatSync(current);
645
+ }
646
+ catch (error) {
647
+ if (!isErrno(error, 'ENOENT'))
648
+ throw error;
649
+ if (current === root)
650
+ break;
651
+ current = dirname(current);
652
+ continue;
653
+ }
654
+ if (info.isSymbolicLink() || !info.isDirectory()) {
655
+ throw new Error('unsafe_migration_state_directory');
656
+ }
657
+ if (process.platform === 'win32') {
658
+ windowsChecks.push({ path: current, parentOnly: windowsChecks.length > 0 });
659
+ }
660
+ else {
661
+ if (uid !== undefined && info.uid !== uid && (nearestExisting || info.uid !== 0)) {
662
+ throw new Error('migration_state_directory_untrusted_owner');
663
+ }
664
+ const writableByOthers = (info.mode & 0o022) !== 0;
665
+ const trustedStickyAncestor = !nearestExisting
666
+ && privateUserAnchor
667
+ && info.uid === 0
668
+ && (info.mode & 0o1000) !== 0;
669
+ if (writableByOthers && !trustedStickyAncestor) {
670
+ throw new Error('migration_state_directory_group_or_world_writable');
671
+ }
672
+ if (info.uid === uid && (info.mode & 0o077) === 0)
673
+ privateUserAnchor = true;
674
+ }
675
+ nearestExisting = false;
676
+ if (current === root)
677
+ break;
678
+ current = dirname(current);
679
+ }
680
+ assertWindowsMigrationAclTrusted(windowsChecks);
681
+ }
682
+ function unresolvedLegacyMigrationOwnership(env, readFile) {
683
+ try {
684
+ const raw = readFile(join(resolveBootstrapStateDir(env), BOOTSTRAP_ATTEMPT_FILE));
685
+ if (raw.length === 0 || raw.length > 4 * 1024)
686
+ return false;
687
+ const parsed = JSON.parse(raw);
688
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
689
+ return false;
690
+ const outcome = parsed['outcome'];
691
+ return outcome === 'migration_ambiguous' || outcome === 'migrated' || outcome === 'completed';
692
+ }
693
+ catch {
694
+ return false;
695
+ }
696
+ }
697
+ async function assertTrustedMigrationDirectoryChain(directory, effectiveUid = resolveProcessUid()) {
698
+ let current = resolvePath(directory);
699
+ const root = parsePath(current).root;
700
+ const windowsChecks = [];
701
+ const uid = effectiveUid;
702
+ let nearestExisting = true;
703
+ let privateUserAnchor = false;
704
+ for (;;) {
705
+ let info;
706
+ try {
707
+ info = await lstat(current);
708
+ }
709
+ catch (error) {
710
+ if (!isErrno(error, 'ENOENT'))
711
+ throw error;
712
+ if (current === root)
713
+ break;
714
+ current = dirname(current);
715
+ continue;
716
+ }
717
+ if (info.isSymbolicLink() || !info.isDirectory()) {
718
+ throw new Error('unsafe_migration_target_directory');
719
+ }
720
+ if (process.platform === 'win32') {
721
+ windowsChecks.push({ path: current, parentOnly: windowsChecks.length > 0 });
722
+ }
723
+ else {
724
+ if (uid !== undefined && info.uid !== uid && (nearestExisting || info.uid !== 0)) {
725
+ throw new Error('migration_target_directory_untrusted_owner');
726
+ }
727
+ const writableByOthers = (info.mode & 0o022) !== 0;
728
+ const trustedStickyAncestor = !nearestExisting
729
+ && privateUserAnchor
730
+ && info.uid === 0
731
+ && (info.mode & 0o1000) !== 0;
732
+ if (writableByOthers && !trustedStickyAncestor) {
733
+ throw new Error('migration_target_directory_group_or_world_writable');
734
+ }
735
+ if (info.uid === uid && (info.mode & 0o077) === 0)
736
+ privateUserAnchor = true;
737
+ }
738
+ nearestExisting = false;
739
+ if (current === root)
740
+ break;
741
+ current = dirname(current);
742
+ }
743
+ assertWindowsMigrationAclTrusted(windowsChecks);
744
+ }
745
+ async function assertTrustedMigrationFile(path, effectiveUid = resolveProcessUid()) {
746
+ const info = await lstat(path);
747
+ if (info.isSymbolicLink() || !info.isFile()) {
748
+ throw new Error('destination_exists_unowned_or_invalid');
749
+ }
750
+ if (process.platform === 'win32') {
751
+ assertWindowsMigrationAclTrusted([{ path, parentOnly: false }]);
752
+ return;
753
+ }
754
+ const uid = effectiveUid;
755
+ if (uid !== undefined && info.uid !== uid) {
756
+ throw new Error('migration_target_file_untrusted_owner');
757
+ }
758
+ if ((info.mode & 0o022) !== 0) {
759
+ throw new Error('migration_target_file_group_or_world_writable');
760
+ }
761
+ }
762
+ async function ensureSafeMigrationTargetDirectory(home, directory, mkdir, assertDirectoryTrust) {
763
+ const absoluteHome = resolvePath(home);
764
+ const absoluteDirectory = resolvePath(directory);
765
+ if (relative(absoluteHome, absoluteDirectory) !== 'bin') {
766
+ throw new Error('migration_target_outside_home');
767
+ }
768
+ await ensureSafeMigrationDirectory(absoluteDirectory, mkdir, assertDirectoryTrust);
769
+ }
770
+ async function assertSafeExistingMigrationTargetDirectory(home, directory, assertDirectoryTrust) {
771
+ const absoluteHome = resolvePath(home);
772
+ const absoluteDirectory = resolvePath(directory);
773
+ if (relative(absoluteHome, absoluteDirectory) !== 'bin') {
774
+ throw new Error('migration_target_outside_home');
775
+ }
776
+ await assertDirectoryTrust(absoluteDirectory);
777
+ let info;
778
+ try {
779
+ info = await lstat(absoluteDirectory);
780
+ }
781
+ catch (error) {
782
+ if (isErrno(error, 'ENOENT'))
783
+ return;
784
+ throw error;
785
+ }
786
+ if (info.isSymbolicLink() || !info.isDirectory()) {
787
+ throw new Error('unsafe_migration_target_directory');
788
+ }
789
+ const canonicalDirectory = await fsRealpath(absoluteDirectory);
790
+ if (migrationPathKey(canonicalDirectory) !== migrationPathKey(absoluteDirectory)) {
791
+ throw new Error('unsafe_migration_target_directory');
792
+ }
793
+ }
794
+ async function ensureSafeMigrationDirectory(directory, mkdir, assertDirectoryTrust) {
795
+ const absoluteDirectory = resolvePath(directory);
796
+ // Validate the existing ancestor before mkdir, then validate the complete canonical tree.
797
+ // This rejects symlinked or non-directory components instead of writing through them.
798
+ await assertDirectoryTrust(absoluteDirectory);
799
+ await mkdir(absoluteDirectory, 0o700);
800
+ await assertDirectoryTrust(absoluteDirectory);
801
+ const canonicalDirectory = await fsRealpath(absoluteDirectory);
802
+ if (migrationPathKey(canonicalDirectory) !== migrationPathKey(absoluteDirectory)) {
803
+ throw new Error('unsafe_migration_target_directory');
804
+ }
805
+ }
806
+ function sha256(bytes) {
807
+ return createHash('sha256').update(bytes).digest('hex');
808
+ }
809
+ async function inspectExistingMigrationTarget(destPath, version, preflight, readBinary, expected, assertDirectoryTrust, assertFileTrust) {
810
+ let before;
811
+ try {
812
+ before = await lstat(destPath, { bigint: true });
813
+ }
814
+ catch (error) {
815
+ if (isErrno(error, 'ENOENT'))
816
+ return undefined;
817
+ throw error;
818
+ }
819
+ if (before.isSymbolicLink() || !before.isFile() || before.dev <= 0n || before.ino <= 0n) {
820
+ throw new Error('destination_exists_unowned_or_invalid');
821
+ }
822
+ await assertDirectoryTrust(dirname(destPath));
823
+ await assertFileTrust(destPath);
824
+ if (before.nlink !== 1n)
825
+ throw new Error('destination_exists_with_external_hardlink');
826
+ if (before.size !== BigInt(expected.size))
827
+ throw new Error('destination_exists_with_different_artifact');
828
+ const existingBytes = await readBinary(destPath);
829
+ if (existingBytes.byteLength !== expected.size || sha256(existingBytes) !== expected.sha256) {
830
+ throw new Error('destination_exists_with_different_artifact');
831
+ }
832
+ try {
833
+ await preflight(destPath, version);
834
+ }
835
+ catch (error) {
836
+ throw new Error(`destination_exists_unowned_or_invalid:${errorDetail(error)}`, { cause: error });
837
+ }
838
+ await assertFileTrust(destPath);
839
+ const after = await lstat(destPath, { bigint: true });
840
+ if (!after.isFile() || after.isSymbolicLink() || after.nlink !== 1n
841
+ || after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size
842
+ || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) {
843
+ throw new Error('destination_changed_during_validation');
844
+ }
845
+ return {
846
+ path: resolvePath(destPath),
847
+ device: before.dev.toString(),
848
+ inode: before.ino.toString(),
849
+ size: expected.size,
850
+ linkCount: 1,
851
+ mtimeNs: before.mtimeNs.toString(),
852
+ ctimeNs: before.ctimeNs.toString(),
853
+ sha256: expected.sha256,
854
+ };
855
+ }
856
+ function sameMigrationTargetIdentity(left, right) {
857
+ return left.path === right.path
858
+ && left.device === right.device
859
+ && left.inode === right.inode
860
+ && left.size === right.size
861
+ && left.linkCount === right.linkCount
862
+ && left.mtimeNs === right.mtimeNs
863
+ && left.ctimeNs === right.ctimeNs
864
+ && left.sha256 === right.sha256;
865
+ }
866
+ function assertMigrationTargetIdentityShape(identity) {
867
+ if (!isAbsolute(identity.path) || migrationPathKey(identity.path) !== migrationPathKey(resolvePath(identity.path))) {
868
+ throw new Error('migration_target_identity_path_invalid');
869
+ }
870
+ if (!Number.isSafeInteger(identity.size) || identity.size < 0
871
+ || identity.linkCount !== 1
872
+ || !/^[0-9]+$/.test(identity.device)
873
+ || !/^[0-9]+$/.test(identity.inode)
874
+ || !/^-?[0-9]+$/.test(identity.mtimeNs)
875
+ || !/^-?[0-9]+$/.test(identity.ctimeNs)
876
+ || !/^[0-9a-f]{64}$/.test(identity.sha256)) {
877
+ throw new Error('migration_target_identity_invalid');
878
+ }
879
+ }
880
+ /**
881
+ * Revalidate the complete signed executable identity immediately before lifecycle bootstrap
882
+ * spawns it. Callers must not substitute the request's presentation object for this check.
883
+ */
884
+ async function assertMigrationTargetIdentityCurrent(identity, expectedVersion, options = {}) {
885
+ assertMigrationTargetIdentityShape(identity);
886
+ const readBinary = options.readBinary ?? ((path) => fsReadFile(path));
887
+ const preflight = options.preflightFn
888
+ ?? ((path, version) => preflightManagedStagedBinary(path, version));
889
+ const assertDirectoryTrust = options.assertDirectoryTrust ?? assertTrustedMigrationDirectoryChain;
890
+ const assertFileTrust = options.assertFileTrust ?? assertTrustedMigrationFile;
891
+ const current = await inspectExistingMigrationTarget(identity.path, expectedVersion, preflight, readBinary, { sha256: identity.sha256, size: identity.size }, assertDirectoryTrust, assertFileTrust);
892
+ if (!current || !sameMigrationTargetIdentity(current, identity)) {
893
+ throw new Error('migration_target_identity_changed');
894
+ }
895
+ }
896
+ const MAX_MIGRATION_LOCK_BYTES = 4_096;
897
+ function assertMigrationLockSnapshotShape(snapshot) {
898
+ if (snapshot.isSymbolicLink()
899
+ || !snapshot.isFile()
900
+ || snapshot.nlink !== 1n
901
+ || snapshot.size > BigInt(MAX_MIGRATION_LOCK_BYTES)) {
902
+ throw new Error('migration_lock_ownership_changed');
903
+ }
904
+ }
905
+ function sameMigrationLockSnapshot(left, right) {
906
+ return left.dev === right.dev
907
+ && left.ino === right.ino
908
+ && left.nlink === 1n
909
+ && right.nlink === 1n
910
+ && left.size === right.size
911
+ && left.mtimeNs === right.mtimeNs
912
+ && left.ctimeNs === right.ctimeNs;
913
+ }
914
+ function readBoundedMigrationLockFd(fd) {
915
+ const bytes = Buffer.allocUnsafe(MAX_MIGRATION_LOCK_BYTES + 1);
916
+ let offset = 0;
917
+ while (offset < bytes.byteLength) {
918
+ const read = readSync(fd, bytes, offset, bytes.byteLength - offset, offset);
919
+ if (read === 0)
920
+ break;
921
+ offset += read;
922
+ }
923
+ if (offset > MAX_MIGRATION_LOCK_BYTES)
924
+ throw new Error('migration_lock_ownership_changed');
925
+ return bytes.subarray(0, offset);
926
+ }
927
+ function readMigrationLockAuthority(lockPath) {
928
+ const beforeOpen = lstatSync(lockPath, { bigint: true });
929
+ assertMigrationLockSnapshotShape(beforeOpen);
930
+ const fd = openSync(lockPath, 'r');
931
+ try {
932
+ const opened = fstatSync(fd, { bigint: true });
933
+ assertMigrationLockSnapshotShape(opened);
934
+ if (!sameMigrationLockSnapshot(beforeOpen, opened)) {
935
+ throw new Error('migration_lock_ownership_changed');
936
+ }
937
+ const firstBytes = readBoundedMigrationLockFd(fd);
938
+ const afterFirstFd = fstatSync(fd, { bigint: true });
939
+ const afterFirstPath = lstatSync(lockPath, { bigint: true });
940
+ const secondBytes = readBoundedMigrationLockFd(fd);
941
+ const finalFd = fstatSync(fd, { bigint: true });
942
+ const finalPath = lstatSync(lockPath, { bigint: true });
943
+ for (const snapshot of [afterFirstFd, afterFirstPath, finalFd, finalPath]) {
944
+ assertMigrationLockSnapshotShape(snapshot);
945
+ if (!sameMigrationLockSnapshot(opened, snapshot)) {
946
+ throw new Error('migration_lock_ownership_changed');
947
+ }
948
+ }
949
+ if (opened.size !== BigInt(firstBytes.byteLength)
950
+ || finalFd.size !== BigInt(secondBytes.byteLength)
951
+ || !firstBytes.equals(secondBytes)) {
952
+ throw new Error('migration_lock_ownership_changed');
953
+ }
954
+ return { snapshot: opened, bytes: Buffer.from(firstBytes) };
955
+ }
956
+ finally {
957
+ closeSync(fd);
958
+ }
959
+ }
960
+ function assertMigrationLockOwnerRecord(bytes, owner) {
961
+ let current;
962
+ try {
963
+ current = JSON.parse(bytes.toString('utf8'));
964
+ }
965
+ catch {
966
+ throw new Error('migration_lock_ownership_changed');
967
+ }
968
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
969
+ throw new Error('migration_lock_ownership_changed');
970
+ }
971
+ const record = current;
972
+ if (record['pid'] !== owner.pid
973
+ || record['token'] !== owner.token
974
+ || JSON.stringify(record['processStart']) !== JSON.stringify(owner.processStartIdentity)) {
975
+ throw new Error('migration_lock_ownership_changed');
976
+ }
977
+ }
978
+ async function withMigrationLock(directory, operation) {
979
+ const lockPath = join(directory, MIGRATION_LOCK_FILE);
980
+ let owner;
981
+ try {
982
+ owner = util.acquireLock(lockPath, { maxTries: 1, waitMs: 0 });
983
+ }
984
+ catch (error) {
985
+ const code = typeof error === 'object' && error !== null ? error.code : undefined;
986
+ throw new Error(code === 'LOCK_TIMEOUT' ? 'migration_lock_busy' : `migration_lock_unsafe:${errorDetail(error)}`, {
987
+ cause: error,
988
+ });
989
+ }
990
+ let acquiredAuthority;
991
+ try {
992
+ acquiredAuthority = readMigrationLockAuthority(lockPath);
993
+ assertMigrationLockOwnerRecord(acquiredAuthority.bytes, owner);
994
+ }
995
+ catch (error) {
996
+ util.releaseLock(lockPath);
997
+ throw error;
998
+ }
999
+ let completed = false;
1000
+ let value;
1001
+ let operationError;
1002
+ const assertOwnershipCurrent = () => {
1003
+ const currentAuthority = readMigrationLockAuthority(lockPath);
1004
+ if (!sameMigrationLockSnapshot(acquiredAuthority.snapshot, currentAuthority.snapshot)
1005
+ || !acquiredAuthority.bytes.equals(currentAuthority.bytes)) {
1006
+ throw new Error('migration_lock_ownership_changed');
1007
+ }
1008
+ assertMigrationLockOwnerRecord(currentAuthority.bytes, owner);
1009
+ };
1010
+ try {
1011
+ assertOwnershipCurrent();
1012
+ value = await operation(assertOwnershipCurrent);
1013
+ assertOwnershipCurrent();
1014
+ completed = true;
1015
+ }
1016
+ catch (error) {
1017
+ operationError = error;
1018
+ }
1019
+ const released = util.releaseLock(lockPath);
1020
+ if (!released.released
1021
+ || (released.reason !== 'released' && released.reason !== 'released_with_cleanup_error')) {
1022
+ throw new Error(`migration_lock_release_failed:${released.reason}`, { cause: operationError });
1023
+ }
1024
+ if (!completed)
1025
+ throw operationError;
1026
+ return value;
1027
+ }
1028
+ async function installVerifiedMigrationBinary(input) {
1029
+ const directory = dirname(input.destPath);
1030
+ // Read-only validation and convergence detection happen before the durable plan. The plan is
1031
+ // then published before mkdir or O_EXCL temporary-file creation touches the target namespace.
1032
+ await input.assertDirectoryTrust(directory);
1033
+ let directoryCreated = false;
1034
+ try {
1035
+ await lstat(directory);
1036
+ }
1037
+ catch (error) {
1038
+ if (!isErrno(error, 'ENOENT'))
1039
+ throw error;
1040
+ directoryCreated = true;
1041
+ }
1042
+ const existingIdentity = await inspectExistingMigrationTarget(input.destPath, input.version, input.preflight, input.readBinary, { sha256: input.expectedSha256, size: input.bytes.byteLength }, input.assertDirectoryTrust, input.assertFileTrust);
1043
+ if (existingIdentity) {
1044
+ return { disposition: 'converged', targetIdentity: existingIdentity, directoryCreated: false };
1045
+ }
1046
+ const temporaryPath = join(directory, `.${parsePath(input.destPath).base}.${process.pid}.${randomUUID()}.migration`);
1047
+ const plannedAnchor = {
1048
+ phase: 'planned',
1049
+ temporaryPath,
1050
+ size: input.bytes.byteLength,
1051
+ sha256: input.expectedSha256,
1052
+ directoryCreated,
1053
+ };
1054
+ await input.publishInstallIntent(plannedAnchor);
1055
+ let publishedAnchor;
1056
+ let materializedAnchor;
1057
+ let concurrentIdentity;
1058
+ try {
1059
+ await ensureSafeMigrationTargetDirectory(input.home, directory, input.mkdir, input.assertDirectoryTrust);
1060
+ if (directoryCreated) {
1061
+ await input.syncDirectory(dirname(directory));
1062
+ }
1063
+ await input.writeBinary(temporaryPath, input.bytes, 0o755);
1064
+ await input.chmod(temporaryPath, 0o755);
1065
+ await input.syncFile(temporaryPath);
1066
+ const temporaryInfo = await input.stat(temporaryPath);
1067
+ const temporaryIdentity = await lstat(temporaryPath, { bigint: true });
1068
+ if (temporaryInfo.isSymbolicLink() || !temporaryInfo.isFile()
1069
+ || temporaryIdentity.isSymbolicLink() || !temporaryIdentity.isFile()
1070
+ || temporaryIdentity.nlink !== 1n || temporaryIdentity.dev <= 0n || temporaryIdentity.ino <= 0n) {
1071
+ throw new Error('migration_temporary_not_owned_regular_file');
1072
+ }
1073
+ const publishedBytes = await input.readBinary(temporaryPath);
1074
+ if (publishedBytes.byteLength !== input.bytes.byteLength || sha256(publishedBytes) !== input.expectedSha256) {
1075
+ throw new Error('migration_temporary_hash_mismatch');
1076
+ }
1077
+ if (temporaryIdentity.size !== BigInt(input.bytes.byteLength)) {
1078
+ throw new Error('migration_temporary_size_mismatch');
1079
+ }
1080
+ materializedAnchor = {
1081
+ phase: 'materialized',
1082
+ temporaryPath,
1083
+ device: temporaryIdentity.dev.toString(),
1084
+ inode: temporaryIdentity.ino.toString(),
1085
+ size: input.bytes.byteLength,
1086
+ sha256: input.expectedSha256,
1087
+ directoryCreated,
1088
+ };
1089
+ await input.publishInstallIntent(materializedAnchor);
1090
+ try {
1091
+ await input.link(temporaryPath, input.destPath);
1092
+ publishedAnchor = {
1093
+ device: temporaryIdentity.dev,
1094
+ inode: temporaryIdentity.ino,
1095
+ size: temporaryIdentity.size,
1096
+ };
1097
+ await input.syncDirectory(directory);
1098
+ }
1099
+ catch (error) {
1100
+ if (!isErrno(error, 'EEXIST'))
1101
+ throw error;
1102
+ concurrentIdentity = await inspectExistingMigrationTarget(input.destPath, input.version, input.preflight, input.readBinary, { sha256: input.expectedSha256, size: input.bytes.byteLength }, input.assertDirectoryTrust, input.assertFileTrust);
1103
+ if (!concurrentIdentity)
1104
+ throw error;
1105
+ }
1106
+ }
1107
+ catch (error) {
1108
+ if (!materializedAnchor) {
1109
+ throw new Error(`migration_install_rollback_unconfirmed:planned_materialization_unconfirmed:${errorDetail(error)}`, { cause: error });
1110
+ }
1111
+ try {
1112
+ await removeMaterializedMigrationTemporaryFile({
1113
+ anchor: materializedAnchor,
1114
+ readBinary: input.readBinary,
1115
+ rename: input.rename,
1116
+ link: input.link,
1117
+ unlink: input.unlink,
1118
+ syncDirectory: input.syncDirectory,
1119
+ assertFileTrust: input.assertFileTrust,
1120
+ });
1121
+ if (publishedAnchor) {
1122
+ await rollbackPublishedMigrationTarget(input, publishedAnchor, materializedAnchor, directoryCreated);
1123
+ }
1124
+ else if (directoryCreated) {
1125
+ await removeMigrationDirectoryIfEmpty(directory, input.rmdir);
1126
+ }
1127
+ }
1128
+ catch (rollbackError) {
1129
+ throw new Error(`migration_install_rollback_unconfirmed:${errorDetail(rollbackError)}`, { cause: error });
1130
+ }
1131
+ throw error;
1132
+ }
1133
+ try {
1134
+ // Remove our temporary hardlink before enforcing the final target's nlink=1 invariant.
1135
+ if (!materializedAnchor)
1136
+ throw new Error('migration_materialized_anchor_missing');
1137
+ await removeMaterializedMigrationTemporaryFile({
1138
+ anchor: materializedAnchor,
1139
+ readBinary: input.readBinary,
1140
+ rename: input.rename,
1141
+ link: input.link,
1142
+ unlink: input.unlink,
1143
+ syncDirectory: input.syncDirectory,
1144
+ assertFileTrust: input.assertFileTrust,
1145
+ });
1146
+ }
1147
+ catch (error) {
1148
+ throw new Error(`migration_install_rollback_unconfirmed:temporary_cleanup:${errorDetail(error)}`);
1149
+ }
1150
+ if (concurrentIdentity) {
1151
+ return { disposition: 'converged', targetIdentity: concurrentIdentity, directoryCreated: false };
1152
+ }
1153
+ if (!publishedAnchor)
1154
+ throw new Error('published_migration_target_missing');
1155
+ try {
1156
+ const installedIdentity = await inspectExistingMigrationTarget(input.destPath, input.version, input.preflight, input.readBinary, { sha256: input.expectedSha256, size: input.bytes.byteLength }, input.assertDirectoryTrust, input.assertFileTrust);
1157
+ if (!installedIdentity)
1158
+ throw new Error('published_migration_target_missing');
1159
+ return { disposition: 'installed', targetIdentity: installedIdentity, directoryCreated };
1160
+ }
1161
+ catch (error) {
1162
+ try {
1163
+ await rollbackPublishedMigrationTarget(input, publishedAnchor, materializedAnchor, directoryCreated);
1164
+ }
1165
+ catch (rollbackError) {
1166
+ throw new Error(`migration_install_rollback_unconfirmed:${errorDetail(rollbackError)}`, { cause: error });
1167
+ }
1168
+ throw error;
1169
+ }
1170
+ }
1171
+ function migrationTemporaryCleanupQuarantinePath(temporaryPath) {
1172
+ return `${temporaryPath}.cleanup`;
1173
+ }
1174
+ function migrationInstallTargetQuarantinePath(anchor) {
1175
+ return `${anchor.temporaryPath}.target-rollback`;
1176
+ }
1177
+ function migrationRegistrationTargetQuarantinePath(destPath, intentId) {
1178
+ return join(dirname(destPath), `.${parsePath(destPath).base}.${intentId}.rollback`);
1179
+ }
1180
+ async function migrationPathIdentity(path) {
1181
+ try {
1182
+ return await lstat(path, { bigint: true });
1183
+ }
1184
+ catch (error) {
1185
+ if (isErrno(error, 'ENOENT'))
1186
+ return undefined;
1187
+ throw error;
1188
+ }
1189
+ }
1190
+ function sameMigrationFileId(left, right) {
1191
+ return left.dev === right.dev && left.ino === right.ino;
1192
+ }
1193
+ async function inspectExactMigrationCleanupFile(input) {
1194
+ const before = await lstat(input.path, { bigint: true });
1195
+ await input.assertFileTrust(input.path);
1196
+ if (before.isSymbolicLink()
1197
+ || !before.isFile()
1198
+ || !input.allowedLinkCounts.includes(before.nlink)
1199
+ || before.dev.toString() !== input.expected.device
1200
+ || before.ino.toString() !== input.expected.inode
1201
+ || before.size !== BigInt(input.expected.size)) {
1202
+ throw new Error(input.identityError);
1203
+ }
1204
+ const bytes = await input.readBinary(input.path);
1205
+ if (bytes.byteLength !== input.expected.size || sha256(bytes) !== input.expected.sha256) {
1206
+ throw new Error(input.hashError);
1207
+ }
1208
+ const after = await lstat(input.path, { bigint: true });
1209
+ if (after.isSymbolicLink()
1210
+ || !after.isFile()
1211
+ || after.dev !== before.dev
1212
+ || after.ino !== before.ino
1213
+ || after.nlink !== before.nlink
1214
+ || after.size !== before.size
1215
+ || after.mtimeNs !== before.mtimeNs
1216
+ || after.ctimeNs !== before.ctimeNs) {
1217
+ throw new Error(input.identityError);
1218
+ }
1219
+ return after;
1220
+ }
1221
+ async function restoreMismatchedMigrationQuarantine(input) {
1222
+ const directory = dirname(input.sourcePath);
1223
+ const sourceIdentity = await migrationPathIdentity(input.sourcePath);
1224
+ if (sourceIdentity && !sameMigrationFileId(sourceIdentity, input.quarantineIdentity)) {
1225
+ throw new Error(`${input.errorPrefix}_quarantine_preserved_canonical_occupied`, { cause: input.reason });
1226
+ }
1227
+ if (!sourceIdentity) {
1228
+ try {
1229
+ await input.link(input.quarantinePath, input.sourcePath);
1230
+ await input.syncDirectory(directory);
1231
+ }
1232
+ catch (error) {
1233
+ if (!isErrno(error, 'EEXIST'))
1234
+ throw error;
1235
+ throw new Error(`${input.errorPrefix}_quarantine_preserved_restore_conflict`, { cause: input.reason });
1236
+ }
1237
+ }
1238
+ const restoredIdentity = await migrationPathIdentity(input.sourcePath);
1239
+ const currentQuarantineIdentity = await migrationPathIdentity(input.quarantinePath);
1240
+ if (!restoredIdentity || !currentQuarantineIdentity
1241
+ || !sameMigrationFileId(restoredIdentity, currentQuarantineIdentity)
1242
+ || !sameMigrationFileId(currentQuarantineIdentity, input.quarantineIdentity)) {
1243
+ throw new Error(`${input.errorPrefix}_quarantine_preserved_restore_inconclusive`, { cause: input.reason });
1244
+ }
1245
+ await input.unlink(input.quarantinePath);
1246
+ await input.syncDirectory(directory);
1247
+ throw new Error(`${input.errorPrefix}_moved_object_restored`, { cause: input.reason });
1248
+ }
1249
+ async function quarantineAndRemoveExactMigrationFile(input) {
1250
+ const directory = dirname(input.sourcePath);
1251
+ let quarantineIdentity = await migrationPathIdentity(input.quarantinePath);
1252
+ if (quarantineIdentity) {
108
1253
  try {
109
- const spawnOptions = { stdio: 'ignore', windowsHide: true, env: childEnv };
110
- child = spawnFn(invocation.command, args, spawnOptions);
1254
+ quarantineIdentity = await inspectExactMigrationCleanupFile({
1255
+ path: input.quarantinePath,
1256
+ expected: input.expected,
1257
+ allowedLinkCounts: input.allowedLinkCounts,
1258
+ readBinary: input.readBinary,
1259
+ assertFileTrust: input.assertFileTrust,
1260
+ identityError: input.identityError,
1261
+ hashError: input.hashError,
1262
+ });
111
1263
  }
112
1264
  catch (error) {
113
- resolvePromise({ ok: false, reason: 'failed', detail: errorDetail(error) });
1265
+ await restoreMismatchedMigrationQuarantine({
1266
+ sourcePath: input.sourcePath,
1267
+ quarantinePath: input.quarantinePath,
1268
+ quarantineIdentity,
1269
+ link: input.link,
1270
+ unlink: input.unlink,
1271
+ syncDirectory: input.syncDirectory,
1272
+ reason: error,
1273
+ errorPrefix: input.errorPrefix,
1274
+ });
1275
+ }
1276
+ const sourceIdentity = await migrationPathIdentity(input.sourcePath);
1277
+ if (sourceIdentity) {
1278
+ if (!sameMigrationFileId(sourceIdentity, quarantineIdentity)) {
1279
+ throw new Error(`${input.errorPrefix}_quarantine_preserved_canonical_occupied`);
1280
+ }
1281
+ await input.unlink(input.quarantinePath);
1282
+ await input.syncDirectory(directory);
1283
+ quarantineIdentity = undefined;
1284
+ }
1285
+ else {
1286
+ await inspectExactMigrationCleanupFile({
1287
+ path: input.quarantinePath,
1288
+ expected: input.expected,
1289
+ allowedLinkCounts: input.allowedLinkCounts,
1290
+ readBinary: input.readBinary,
1291
+ assertFileTrust: input.assertFileTrust,
1292
+ identityError: input.identityError,
1293
+ hashError: input.hashError,
1294
+ });
1295
+ await input.unlink(input.quarantinePath);
1296
+ await input.syncDirectory(directory);
114
1297
  return;
115
1298
  }
116
- let settled = false;
117
- const settle = (outcome) => {
118
- if (settled)
119
- return;
120
- settled = true;
121
- clearTimeout(timer);
122
- resolvePromise(outcome);
1299
+ }
1300
+ const sourceIdentity = await migrationPathIdentity(input.sourcePath);
1301
+ if (!sourceIdentity)
1302
+ return;
1303
+ await inspectExactMigrationCleanupFile({
1304
+ path: input.sourcePath,
1305
+ expected: input.expected,
1306
+ allowedLinkCounts: input.allowedLinkCounts,
1307
+ readBinary: input.readBinary,
1308
+ assertFileTrust: input.assertFileTrust,
1309
+ identityError: input.identityError,
1310
+ hashError: input.hashError,
1311
+ });
1312
+ if (await migrationPathIdentity(input.quarantinePath)) {
1313
+ throw new Error(`${input.errorPrefix}_quarantine_occupied`);
1314
+ }
1315
+ await input.rename(input.sourcePath, input.quarantinePath);
1316
+ await input.syncDirectory(directory);
1317
+ let movedIdentity = await migrationPathIdentity(input.quarantinePath);
1318
+ if (!movedIdentity)
1319
+ throw new Error(`${input.errorPrefix}_quarantine_missing_after_move`);
1320
+ try {
1321
+ movedIdentity = await inspectExactMigrationCleanupFile({
1322
+ path: input.quarantinePath,
1323
+ expected: input.expected,
1324
+ allowedLinkCounts: input.allowedLinkCounts,
1325
+ readBinary: input.readBinary,
1326
+ assertFileTrust: input.assertFileTrust,
1327
+ identityError: input.identityError,
1328
+ hashError: input.hashError,
1329
+ });
1330
+ }
1331
+ catch (error) {
1332
+ await restoreMismatchedMigrationQuarantine({
1333
+ sourcePath: input.sourcePath,
1334
+ quarantinePath: input.quarantinePath,
1335
+ quarantineIdentity: movedIdentity,
1336
+ link: input.link,
1337
+ unlink: input.unlink,
1338
+ syncDirectory: input.syncDirectory,
1339
+ reason: error,
1340
+ errorPrefix: input.errorPrefix,
1341
+ });
1342
+ }
1343
+ if (await migrationPathIdentity(input.sourcePath)) {
1344
+ throw new Error(`${input.errorPrefix}_quarantine_preserved_canonical_recreated`);
1345
+ }
1346
+ await inspectExactMigrationCleanupFile({
1347
+ path: input.quarantinePath,
1348
+ expected: input.expected,
1349
+ allowedLinkCounts: input.allowedLinkCounts,
1350
+ readBinary: input.readBinary,
1351
+ assertFileTrust: input.assertFileTrust,
1352
+ identityError: input.identityError,
1353
+ hashError: input.hashError,
1354
+ });
1355
+ await input.unlink(input.quarantinePath);
1356
+ await input.syncDirectory(directory);
1357
+ }
1358
+ async function removeMaterializedMigrationTemporaryFile(input) {
1359
+ await quarantineAndRemoveExactMigrationFile({
1360
+ sourcePath: input.anchor.temporaryPath,
1361
+ quarantinePath: migrationTemporaryCleanupQuarantinePath(input.anchor.temporaryPath),
1362
+ expected: input.anchor,
1363
+ allowedLinkCounts: [1n, 2n],
1364
+ readBinary: input.readBinary,
1365
+ rename: input.rename,
1366
+ link: input.link,
1367
+ unlink: input.unlink,
1368
+ syncDirectory: input.syncDirectory,
1369
+ assertFileTrust: input.assertFileTrust,
1370
+ identityError: 'migration_materialized_temporary_identity_changed',
1371
+ hashError: 'migration_materialized_temporary_hash_changed',
1372
+ errorPrefix: 'migration_materialized_temporary_cleanup',
1373
+ });
1374
+ }
1375
+ function parsePendingMigrationInstall(raw) {
1376
+ const record = parseCanonicalMigrationState(raw);
1377
+ if (!record)
1378
+ throw new Error('migration_install_journal_invalid');
1379
+ if (record.state !== 'installing')
1380
+ return undefined;
1381
+ return {
1382
+ version: record.version,
1383
+ destPath: record.destPath,
1384
+ anchor: record.installAnchor,
1385
+ };
1386
+ }
1387
+ async function recoverPendingMigrationInstall(input) {
1388
+ const initialAuthoritativePayload = readTrustedMigrationStateText(join(resolveBootstrapStateDir(input.env), MIGRATION_STATE_FILE), input.stateOptions);
1389
+ if (initialAuthoritativePayload === undefined)
1390
+ return false;
1391
+ let authoritativePayload = initialAuthoritativePayload;
1392
+ const pending = parsePendingMigrationInstall(authoritativePayload);
1393
+ if (!pending)
1394
+ return false;
1395
+ input.markCanonicalStateActive();
1396
+ const directory = dirname(pending.destPath);
1397
+ const storedHome = dirname(directory);
1398
+ const assertPendingCurrent = () => {
1399
+ input.assertOwnerCurrent();
1400
+ assertAuthoritativeMigrationStateCurrent(input.env, authoritativePayload, input.stateOptions);
1401
+ };
1402
+ assertPendingCurrent();
1403
+ await assertSafeExistingMigrationTargetDirectory(storedHome, directory, input.assertDirectoryTrust);
1404
+ const completeRecovery = () => {
1405
+ assertPendingCurrent();
1406
+ persistAuthoritativeMigrationState(input.env, {
1407
+ state: 'rolled_back',
1408
+ version: pending.version,
1409
+ destPath: pending.destPath,
1410
+ reason: 'install_recovered',
1411
+ installAnchor: pending.anchor,
1412
+ }, input.stateOptions);
1413
+ };
1414
+ if (pending.anchor.phase === 'planned') {
1415
+ let before;
1416
+ try {
1417
+ before = await lstat(pending.anchor.temporaryPath, { bigint: true });
1418
+ }
1419
+ catch (error) {
1420
+ if (!isErrno(error, 'ENOENT'))
1421
+ throw error;
1422
+ if (pending.anchor.directoryCreated) {
1423
+ assertPendingCurrent();
1424
+ await removeMigrationDirectoryIfEmpty(directory, input.rmdir);
1425
+ await input.syncDirectory(dirname(directory));
1426
+ }
1427
+ completeRecovery();
1428
+ return true;
1429
+ }
1430
+ await input.assertFileTrust(pending.anchor.temporaryPath);
1431
+ if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1n
1432
+ || before.dev <= 0n || before.ino <= 0n || before.size > BigInt(pending.anchor.size)) {
1433
+ throw new Error('migration_planned_temporary_unowned');
1434
+ }
1435
+ const bytes = await input.readBinary(pending.anchor.temporaryPath);
1436
+ const after = await lstat(pending.anchor.temporaryPath, { bigint: true });
1437
+ if (after.isSymbolicLink()
1438
+ || !after.isFile()
1439
+ || after.dev !== before.dev
1440
+ || after.ino !== before.ino
1441
+ || after.nlink !== before.nlink
1442
+ || after.size !== before.size
1443
+ || after.mtimeNs !== before.mtimeNs
1444
+ || after.ctimeNs !== before.ctimeNs) {
1445
+ throw new Error('migration_planned_temporary_identity_changed');
1446
+ }
1447
+ if (bytes.byteLength !== Number(after.size)) {
1448
+ throw new Error('migration_planned_temporary_identity_changed');
1449
+ }
1450
+ const cleanupAnchor = {
1451
+ phase: 'cleanup',
1452
+ temporaryPath: pending.anchor.temporaryPath,
1453
+ device: after.dev.toString(),
1454
+ inode: after.ino.toString(),
1455
+ size: bytes.byteLength,
1456
+ sha256: sha256(bytes),
1457
+ directoryCreated: pending.anchor.directoryCreated,
123
1458
  };
124
- const timer = setTimeout(() => {
125
- child.kill();
126
- settle({ ok: false, reason: 'timeout' });
127
- }, timeoutMs);
128
- child.once('error', (error) => {
129
- settle({ ok: false, reason: 'failed', detail: error.message });
1459
+ assertPendingCurrent();
1460
+ authoritativePayload = persistAuthoritativeMigrationState(input.env, {
1461
+ state: 'installing',
1462
+ version: pending.version,
1463
+ destPath: pending.destPath,
1464
+ installAnchor: cleanupAnchor,
1465
+ }, input.stateOptions);
1466
+ await quarantineAndRemoveExactMigrationFile({
1467
+ sourcePath: cleanupAnchor.temporaryPath,
1468
+ quarantinePath: migrationTemporaryCleanupQuarantinePath(cleanupAnchor.temporaryPath),
1469
+ expected: cleanupAnchor,
1470
+ allowedLinkCounts: [1n],
1471
+ readBinary: input.readBinary,
1472
+ rename: input.rename,
1473
+ link: input.link,
1474
+ unlink: input.unlink,
1475
+ syncDirectory: input.syncDirectory,
1476
+ assertFileTrust: input.assertFileTrust,
1477
+ identityError: 'migration_planned_temporary_identity_changed',
1478
+ hashError: 'migration_planned_temporary_hash_changed',
1479
+ errorPrefix: 'migration_planned_temporary_cleanup',
130
1480
  });
131
- child.once('exit', (code) => {
132
- if (code === 0)
133
- settle({ ok: true, reason: 'registered' });
134
- else
135
- settle({ ok: false, reason: 'failed', detail: `exit ${code ?? 'null'}` });
1481
+ if (pending.anchor.directoryCreated) {
1482
+ assertPendingCurrent();
1483
+ await removeMigrationDirectoryIfEmpty(directory, input.rmdir);
1484
+ await input.syncDirectory(dirname(directory));
1485
+ }
1486
+ completeRecovery();
1487
+ return true;
1488
+ }
1489
+ if (pending.anchor.phase === 'cleanup') {
1490
+ await quarantineAndRemoveExactMigrationFile({
1491
+ sourcePath: pending.anchor.temporaryPath,
1492
+ quarantinePath: migrationTemporaryCleanupQuarantinePath(pending.anchor.temporaryPath),
1493
+ expected: pending.anchor,
1494
+ allowedLinkCounts: [1n],
1495
+ readBinary: input.readBinary,
1496
+ rename: input.rename,
1497
+ link: input.link,
1498
+ unlink: input.unlink,
1499
+ syncDirectory: input.syncDirectory,
1500
+ assertFileTrust: input.assertFileTrust,
1501
+ identityError: 'migration_planned_temporary_identity_changed',
1502
+ hashError: 'migration_planned_temporary_hash_changed',
1503
+ errorPrefix: 'migration_planned_temporary_cleanup',
136
1504
  });
1505
+ if (pending.anchor.directoryCreated) {
1506
+ assertPendingCurrent();
1507
+ await removeMigrationDirectoryIfEmpty(directory, input.rmdir);
1508
+ await input.syncDirectory(dirname(directory));
1509
+ }
1510
+ completeRecovery();
1511
+ return true;
1512
+ }
1513
+ const materializedAnchor = pending.anchor;
1514
+ assertPendingCurrent();
1515
+ await removeMaterializedMigrationTemporaryFile({
1516
+ anchor: materializedAnchor,
1517
+ readBinary: input.readBinary,
1518
+ rename: input.rename,
1519
+ link: input.link,
1520
+ unlink: input.unlink,
1521
+ syncDirectory: input.syncDirectory,
1522
+ assertFileTrust: input.assertFileTrust,
1523
+ });
1524
+ assertPendingCurrent();
1525
+ await rollbackPublishedMigrationTarget({
1526
+ destPath: pending.destPath,
1527
+ expectedSha256: materializedAnchor.sha256,
1528
+ readBinary: input.readBinary,
1529
+ rename: input.rename,
1530
+ link: input.link,
1531
+ unlink: input.unlink,
1532
+ rmdir: input.rmdir,
1533
+ syncDirectory: input.syncDirectory,
1534
+ assertFileTrust: input.assertFileTrust,
1535
+ }, {
1536
+ device: BigInt(materializedAnchor.device),
1537
+ inode: BigInt(materializedAnchor.inode),
1538
+ size: BigInt(materializedAnchor.size),
1539
+ }, materializedAnchor, false);
1540
+ if (materializedAnchor.directoryCreated) {
1541
+ assertPendingCurrent();
1542
+ await removeMigrationDirectoryIfEmpty(directory, input.rmdir);
1543
+ await input.syncDirectory(dirname(directory));
1544
+ }
1545
+ completeRecovery();
1546
+ return true;
1547
+ }
1548
+ async function removeMigrationDirectoryIfEmpty(directory, rmdir) {
1549
+ try {
1550
+ await rmdir(directory);
1551
+ }
1552
+ catch (error) {
1553
+ if (!isErrno(error, 'ENOENT') && !isErrno(error, 'ENOTEMPTY') && !isErrno(error, 'EEXIST')) {
1554
+ throw error;
1555
+ }
1556
+ }
1557
+ }
1558
+ async function rollbackPublishedMigrationTarget(input, anchor, materializedAnchor, directoryCreated) {
1559
+ await quarantineAndRemoveExactMigrationFile({
1560
+ sourcePath: input.destPath,
1561
+ quarantinePath: migrationInstallTargetQuarantinePath(materializedAnchor),
1562
+ expected: {
1563
+ device: anchor.device.toString(),
1564
+ inode: anchor.inode.toString(),
1565
+ size: Number(anchor.size),
1566
+ sha256: input.expectedSha256,
1567
+ },
1568
+ allowedLinkCounts: [1n],
1569
+ readBinary: input.readBinary,
1570
+ rename: input.rename,
1571
+ link: input.link,
1572
+ unlink: input.unlink,
1573
+ syncDirectory: input.syncDirectory,
1574
+ assertFileTrust: input.assertFileTrust,
1575
+ identityError: 'migration_install_rollback_identity_changed',
1576
+ hashError: 'migration_install_rollback_hash_changed',
1577
+ errorPrefix: 'migration_install_target_cleanup',
1578
+ });
1579
+ if (directoryCreated) {
1580
+ await removeMigrationDirectoryIfEmpty(dirname(input.destPath), input.rmdir);
1581
+ await input.syncDirectory(dirname(dirname(input.destPath)));
1582
+ }
1583
+ }
1584
+ async function cleanupInstalledMigrationBinary(input) {
1585
+ if (input.install.disposition !== 'installed')
1586
+ return;
1587
+ const directory = dirname(input.destPath);
1588
+ await input.assertAuthorityCurrent();
1589
+ await input.assertDirectoryTrust(directory);
1590
+ if (await migrationPathIdentity(input.destPath)) {
1591
+ const currentIdentity = await inspectExistingMigrationTarget(input.destPath, input.version, input.preflight, input.readBinary, { sha256: input.expectedSha256, size: input.install.targetIdentity.size }, input.assertDirectoryTrust, input.assertFileTrust);
1592
+ if (!currentIdentity || !sameMigrationTargetIdentity(currentIdentity, input.install.targetIdentity)) {
1593
+ throw new Error('migration_cleanup_target_identity_changed');
1594
+ }
1595
+ }
1596
+ await input.assertAuthorityCurrent();
1597
+ await quarantineAndRemoveExactMigrationFile({
1598
+ sourcePath: input.destPath,
1599
+ quarantinePath: input.quarantinePath,
1600
+ expected: input.install.targetIdentity,
1601
+ allowedLinkCounts: [1n],
1602
+ readBinary: input.readBinary,
1603
+ rename: input.rename,
1604
+ link: input.link,
1605
+ unlink: input.unlink,
1606
+ syncDirectory: input.syncDirectory,
1607
+ assertFileTrust: input.assertFileTrust,
1608
+ identityError: 'migration_cleanup_target_identity_changed',
1609
+ hashError: 'migration_cleanup_target_hash_changed',
1610
+ errorPrefix: 'migration_cleanup_target',
137
1611
  });
1612
+ if (input.install.directoryCreated) {
1613
+ await removeMigrationDirectoryIfEmpty(directory, input.rmdir);
1614
+ await input.syncDirectory(dirname(directory));
1615
+ }
1616
+ }
1617
+ function isCanonicalMigrationRegistrationState(record) {
1618
+ return record.state === 'registering' || record.state === 'blocked' || record.state === 'committed';
1619
+ }
1620
+ async function completeMigrationRollbackPending(input) {
1621
+ const assertCleanupAuthorityCurrent = async () => {
1622
+ await input.assertOwnerCurrent();
1623
+ assertAuthoritativeMigrationStateCurrent(input.env, input.expectedPayload, input.stateOptions);
1624
+ };
1625
+ const directory = dirname(input.record.destPath);
1626
+ const storedHome = dirname(directory);
1627
+ await assertCleanupAuthorityCurrent();
1628
+ await assertSafeExistingMigrationTargetDirectory(storedHome, directory, input.assertDirectoryTrust);
1629
+ if (input.record.installOwnership.disposition === 'installed') {
1630
+ const quarantinePath = migrationRegistrationTargetQuarantinePath(input.record.destPath, input.record.intentId);
1631
+ let targetPresent = true;
1632
+ try {
1633
+ await lstat(input.record.destPath);
1634
+ }
1635
+ catch (error) {
1636
+ if (!isErrno(error, 'ENOENT'))
1637
+ throw error;
1638
+ targetPresent = false;
1639
+ }
1640
+ const quarantinePresent = await migrationPathIdentity(quarantinePath) !== undefined;
1641
+ if (targetPresent || quarantinePresent) {
1642
+ await cleanupInstalledMigrationBinary({
1643
+ install: {
1644
+ disposition: 'installed',
1645
+ targetIdentity: input.record.targetIdentity,
1646
+ directoryCreated: false,
1647
+ },
1648
+ destPath: input.record.destPath,
1649
+ version: input.record.version,
1650
+ expectedSha256: input.record.targetIdentity.sha256,
1651
+ quarantinePath,
1652
+ readBinary: input.readBinary,
1653
+ preflight: input.preflightFn,
1654
+ rename: input.rename,
1655
+ link: input.link,
1656
+ unlink: input.unlink,
1657
+ rmdir: input.rmdir,
1658
+ syncDirectory: input.syncDirectory,
1659
+ assertDirectoryTrust: input.assertDirectoryTrust,
1660
+ assertFileTrust: input.assertFileTrust,
1661
+ assertAuthorityCurrent: assertCleanupAuthorityCurrent,
1662
+ });
1663
+ }
1664
+ else if (!input.record.installOwnership.directoryCreated) {
1665
+ let directoryPresent = true;
1666
+ try {
1667
+ await lstat(directory);
1668
+ }
1669
+ catch (error) {
1670
+ if (!isErrno(error, 'ENOENT'))
1671
+ throw error;
1672
+ directoryPresent = false;
1673
+ }
1674
+ await assertCleanupAuthorityCurrent();
1675
+ await input.syncDirectory(directoryPresent ? directory : dirname(directory));
1676
+ }
1677
+ if (input.record.installOwnership.directoryCreated) {
1678
+ await assertCleanupAuthorityCurrent();
1679
+ await removeMigrationDirectoryIfEmpty(directory, input.rmdir);
1680
+ await input.syncDirectory(dirname(directory));
1681
+ }
1682
+ }
1683
+ await assertCleanupAuthorityCurrent();
1684
+ persistAuthoritativeMigrationState(input.env, {
1685
+ state: 'rolled_back',
1686
+ version: input.record.version,
1687
+ destPath: input.record.destPath,
1688
+ intentId: input.record.intentId,
1689
+ targetIdentity: input.record.targetIdentity,
1690
+ installOwnership: input.record.installOwnership,
1691
+ reason: input.record.reason,
1692
+ }, input.stateOptions);
1693
+ }
1694
+ async function reconcileStoredMigrationInstall(input) {
1695
+ const { env, options, record, expectedPayload, stateOptions } = input;
1696
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
1697
+ const failClosed = (reason, detail) => ({
1698
+ action: 'return',
1699
+ result: {
1700
+ outcome: 'failed',
1701
+ reason: `${code}:${reason}`,
1702
+ destPath: record.destPath,
1703
+ message: `one-time standalone migration recovery failed (${code}:${reason}:${errorDetail(detail)})`,
1704
+ requiresForegroundExit: true,
1705
+ },
1706
+ });
1707
+ const readBinary = options.readBinary ?? ((path) => fsReadFile(path));
1708
+ const rename = options.rename ?? ((from, to) => fsRename(from, to));
1709
+ const link = options.link ?? ((from, to) => fsLink(from, to));
1710
+ const unlink = options.unlink ?? ((path) => fsUnlink(path));
1711
+ const rmdir = options.rmdir ?? ((path) => fsRmdir(path));
1712
+ const uid = options.uid ?? resolveProcessUid();
1713
+ const assertDirectoryTrust = options.assertDirectoryTrust
1714
+ ?? ((directory) => assertTrustedMigrationDirectoryChain(directory, uid));
1715
+ const assertFileTrust = options.assertFileTrust
1716
+ ?? ((path) => assertTrustedMigrationFile(path, uid));
1717
+ const syncDirectory = options.syncDirectory ?? (async (path) => {
1718
+ let handle;
1719
+ try {
1720
+ handle = await fsOpen(path, 'r');
1721
+ await handle.sync();
1722
+ }
1723
+ catch (error) {
1724
+ if (process.platform !== 'win32')
1725
+ throw error;
1726
+ }
1727
+ finally {
1728
+ await handle?.close();
1729
+ }
1730
+ });
1731
+ try {
1732
+ return await withMigrationLock(resolveBootstrapStateDir(env), async (assertOwnerLeaseCurrent) => {
1733
+ assertOwnerLeaseCurrent();
1734
+ assertAuthoritativeMigrationStateCurrent(env, expectedPayload, stateOptions);
1735
+ const recovered = await recoverPendingMigrationInstall({
1736
+ env,
1737
+ stateOptions,
1738
+ readBinary,
1739
+ rename,
1740
+ link,
1741
+ unlink,
1742
+ rmdir,
1743
+ syncDirectory,
1744
+ assertDirectoryTrust,
1745
+ assertFileTrust,
1746
+ assertOwnerCurrent: assertOwnerLeaseCurrent,
1747
+ markCanonicalStateActive: () => undefined,
1748
+ });
1749
+ if (!recovered)
1750
+ return failClosed('stored_install_changed', 'stored installing state changed');
1751
+ return { action: 'retry_current' };
1752
+ });
1753
+ }
1754
+ catch (error) {
1755
+ if (error instanceof Error && error.message === 'migration_lock_busy') {
1756
+ return failClosed('migration_in_progress', 'another live process owns migration recovery');
1757
+ }
1758
+ return failClosed('recovery_failed', errorDetail(error));
1759
+ }
1760
+ }
1761
+ async function reconcileStoredMigrationRollbackPending(input) {
1762
+ const { env, options, record, expectedPayload, stateOptions } = input;
1763
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
1764
+ const failClosed = (reason, detail) => ({
1765
+ action: 'return',
1766
+ result: {
1767
+ outcome: 'failed',
1768
+ reason: `${code}:${reason}`,
1769
+ destPath: record.destPath,
1770
+ message: `one-time standalone migration rollback recovery failed (${code}:${reason}:${errorDetail(detail)})`,
1771
+ requiresForegroundExit: true,
1772
+ },
1773
+ });
1774
+ const readBinary = options.readBinary ?? ((path) => fsReadFile(path));
1775
+ const preflightFn = options.preflightFn
1776
+ ?? ((targetPath, expectedVersion) => preflightManagedStagedBinary(targetPath, expectedVersion, options.probe));
1777
+ const rename = options.rename ?? ((from, to) => fsRename(from, to));
1778
+ const link = options.link ?? ((from, to) => fsLink(from, to));
1779
+ const unlink = options.unlink ?? ((path) => fsUnlink(path));
1780
+ const rmdir = options.rmdir ?? ((path) => fsRmdir(path));
1781
+ const uid = options.uid ?? resolveProcessUid();
1782
+ const assertDirectoryTrust = options.assertDirectoryTrust
1783
+ ?? ((directory) => assertTrustedMigrationDirectoryChain(directory, uid));
1784
+ const assertFileTrust = options.assertFileTrust
1785
+ ?? ((path) => assertTrustedMigrationFile(path, uid));
1786
+ const syncDirectory = options.syncDirectory ?? (async (path) => {
1787
+ let handle;
1788
+ try {
1789
+ handle = await fsOpen(path, 'r');
1790
+ await handle.sync();
1791
+ }
1792
+ catch (error) {
1793
+ if (process.platform !== 'win32')
1794
+ throw error;
1795
+ }
1796
+ finally {
1797
+ await handle?.close();
1798
+ }
1799
+ });
1800
+ try {
1801
+ return await withMigrationLock(resolveBootstrapStateDir(env), async (assertOwnerLeaseCurrent) => {
1802
+ await completeMigrationRollbackPending({
1803
+ env,
1804
+ record,
1805
+ expectedPayload,
1806
+ stateOptions,
1807
+ readBinary,
1808
+ preflightFn,
1809
+ rename,
1810
+ link,
1811
+ unlink,
1812
+ rmdir,
1813
+ syncDirectory,
1814
+ assertDirectoryTrust,
1815
+ assertFileTrust,
1816
+ assertOwnerCurrent: assertOwnerLeaseCurrent,
1817
+ });
1818
+ return { action: 'retry_current' };
1819
+ });
1820
+ }
1821
+ catch (error) {
1822
+ if (error instanceof Error && error.message === 'migration_lock_busy') {
1823
+ return failClosed('migration_in_progress', 'another live process owns migration rollback recovery');
1824
+ }
1825
+ return failClosed('rollback_cleanup_failed', errorDetail(error));
1826
+ }
1827
+ }
1828
+ async function reconcileStoredMigrationRegistration(input) {
1829
+ const { env, platform, options, record, expectedPayload, stateOptions } = input;
1830
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
1831
+ const failed = (reason, message, _detail, requiresForegroundExit = false) => ({
1832
+ outcome: 'failed',
1833
+ reason,
1834
+ destPath: record.destPath,
1835
+ message,
1836
+ ...(requiresForegroundExit ? { requiresForegroundExit: true } : {}),
1837
+ });
1838
+ const timedOut = (reason, message, detail, requiresForegroundExit) => failed(reason, message, detail, requiresForegroundExit);
1839
+ const failClosed = (reason, detail) => ({
1840
+ action: 'return',
1841
+ result: {
1842
+ outcome: 'failed',
1843
+ reason: `${code}:${reason}`,
1844
+ destPath: record.destPath,
1845
+ message: `one-time standalone migration recovery failed (${code}:${reason}:${errorDetail(detail)})`,
1846
+ requiresForegroundExit: true,
1847
+ },
1848
+ });
1849
+ const timeoutMs = options.timeoutMs ?? MIGRATION_REGISTER_TIMEOUT_MS;
1850
+ const transactionBudgetMs = options.transactionBudgetMs ?? MIGRATION_REGISTER_TRANSACTION_BUDGET_MS;
1851
+ const readBinary = options.readBinary ?? ((path) => fsReadFile(path));
1852
+ const rename = options.rename ?? ((from, to) => fsRename(from, to));
1853
+ const link = options.link ?? ((from, to) => fsLink(from, to));
1854
+ const unlink = options.unlink ?? ((path) => fsUnlink(path));
1855
+ const rmdir = options.rmdir ?? ((path) => fsRmdir(path));
1856
+ const syncDirectory = options.syncDirectory ?? (async (path) => {
1857
+ let handle;
1858
+ try {
1859
+ handle = await fsOpen(path, 'r');
1860
+ await handle.sync();
1861
+ }
1862
+ catch (error) {
1863
+ if (process.platform !== 'win32')
1864
+ throw error;
1865
+ }
1866
+ finally {
1867
+ await handle?.close();
1868
+ }
1869
+ });
1870
+ const preflightFn = options.preflightFn
1871
+ ?? ((targetPath, expectedVersion) => preflightManagedStagedBinary(targetPath, expectedVersion, options.probe));
1872
+ const uid = options.uid ?? resolveProcessUid();
1873
+ const assertDirectoryTrust = options.assertDirectoryTrust
1874
+ ?? ((directory) => assertTrustedMigrationDirectoryChain(directory, uid));
1875
+ const assertFileTrust = options.assertFileTrust
1876
+ ?? ((path) => assertTrustedMigrationFile(path, uid));
1877
+ try {
1878
+ return await withMigrationLock(resolveBootstrapStateDir(env), async (assertOwnerLeaseCurrent) => {
1879
+ const assertStoredIntentCurrent = async () => {
1880
+ assertOwnerLeaseCurrent();
1881
+ assertAuthoritativeMigrationStateCurrent(env, expectedPayload, stateOptions);
1882
+ };
1883
+ const sealedIdentity = Object.freeze({ ...record.targetIdentity });
1884
+ const revalidateStoredTarget = () => assertMigrationTargetIdentityCurrent(sealedIdentity, record.version, { readBinary, preflightFn, assertDirectoryTrust, assertFileTrust });
1885
+ if (!Number.isSafeInteger(timeoutMs)
1886
+ || !Number.isSafeInteger(transactionBudgetMs)
1887
+ || timeoutMs <= transactionBudgetMs
1888
+ || timeoutMs > MAX_TIMER_DELAY_MS
1889
+ || transactionBudgetMs <= 0) {
1890
+ return failClosed('invalid_registration_timeout_contract', 'invalid registration timeout contract');
1891
+ }
1892
+ if (!options.registrationRunner) {
1893
+ return failClosed('registration_runner_unavailable', 'registration runner unavailable');
1894
+ }
1895
+ try {
1896
+ await assertStoredIntentCurrent();
1897
+ await revalidateStoredTarget();
1898
+ }
1899
+ catch (error) {
1900
+ return failClosed('stored_target_revalidation_failed', errorDetail(error));
1901
+ }
1902
+ const registration = await register(env, platform, record.version, record.destPath, sealedIdentity, revalidateStoredTarget, assertStoredIntentCurrent, options, timeoutMs, transactionBudgetMs, failed, timedOut);
1903
+ try {
1904
+ await assertStoredIntentCurrent();
1905
+ }
1906
+ catch (error) {
1907
+ return failClosed('stored_intent_changed', errorDetail(error));
1908
+ }
1909
+ const intentBase = {
1910
+ version: record.version,
1911
+ destPath: record.destPath,
1912
+ intentId: record.intentId,
1913
+ targetIdentity: sealedIdentity,
1914
+ installOwnership: record.installOwnership,
1915
+ };
1916
+ if (registration.outcome === 'migrated') {
1917
+ persistAuthoritativeMigrationState(env, { state: 'committed', ...intentBase }, stateOptions);
1918
+ recordBootstrapAttempt(env, { ok: true, reason: 'migrated', detail: record.destPath }, stateOptions);
1919
+ return { action: 'return', result: registration };
1920
+ }
1921
+ if (registration.requiresForegroundExit === true) {
1922
+ try {
1923
+ persistAuthoritativeMigrationState(env, { state: 'blocked', ...intentBase, reason: registration.reason }, stateOptions);
1924
+ }
1925
+ catch {
1926
+ // Preserve the exact prior active record when blocked-state publication is uncertain.
1927
+ }
1928
+ return { action: 'return', result: registration };
1929
+ }
1930
+ const rollbackPendingRecord = {
1931
+ state: 'rollback_pending',
1932
+ ...intentBase,
1933
+ reason: registration.reason,
1934
+ };
1935
+ let rollbackPendingPayload;
1936
+ try {
1937
+ await assertStoredIntentCurrent();
1938
+ rollbackPendingPayload = persistAuthoritativeMigrationState(env, rollbackPendingRecord, stateOptions);
1939
+ }
1940
+ catch (error) {
1941
+ return failClosed('rollback_intent_persistence_failed', errorDetail(error));
1942
+ }
1943
+ try {
1944
+ await completeMigrationRollbackPending({
1945
+ env,
1946
+ record: { ...intentBase, reason: registration.reason },
1947
+ expectedPayload: rollbackPendingPayload,
1948
+ stateOptions,
1949
+ readBinary,
1950
+ preflightFn,
1951
+ rename,
1952
+ link,
1953
+ unlink,
1954
+ rmdir,
1955
+ syncDirectory,
1956
+ assertDirectoryTrust,
1957
+ assertFileTrust,
1958
+ assertOwnerCurrent: assertOwnerLeaseCurrent,
1959
+ });
1960
+ }
1961
+ catch (error) {
1962
+ return failClosed('stored_target_cleanup_failed', errorDetail(error));
1963
+ }
1964
+ return { action: 'retry_current' };
1965
+ });
1966
+ }
1967
+ catch (error) {
1968
+ if (error instanceof Error && error.message === 'migration_lock_busy') {
1969
+ return failClosed('migration_in_progress', 'another live process owns migration recovery');
1970
+ }
1971
+ return failClosed('stored_reconciliation_failed', errorDetail(error));
1972
+ }
138
1973
  }
139
1974
  /**
140
1975
  * One-time migration of the npm/JS install shape to the standalone release binary.
141
- * Never throws: every failure/skip becomes a structured MigrationResult so degraded
142
- * startup can keep running with self-update off.
1976
+ * Never throws: every failure/skip becomes a structured MigrationResult. Only a proven clean
1977
+ * failure may continue degraded; ambiguous registration ownership requires foreground exit.
143
1978
  */
144
1979
  export async function migrateToStandaloneBinary(env, platform = process.platform, options = {}) {
145
1980
  const now = options.now ?? Date.now();
146
1981
  const exists = options.exists ?? existsSync;
147
1982
  const readText = options.readFile ?? defaultReadTextFile;
148
1983
  const recordOptions = { now, ...(options.writeFile ? { writeFile: options.writeFile } : {}) };
1984
+ const stateOptions = {
1985
+ ...recordOptions,
1986
+ ...(options.assertStateDirectoryTrust
1987
+ ? { assertStateDirectoryTrust: options.assertStateDirectoryTrust }
1988
+ : {}),
1989
+ ...(options.assertStateFileTrust
1990
+ ? { assertStateFileTrust: options.assertStateFileTrust }
1991
+ : {}),
1992
+ };
1993
+ const startupState = inspectMigrationStartupState(env, stateOptions);
1994
+ if (startupState.classification === 'unsafe') {
1995
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
1996
+ return {
1997
+ outcome: 'failed',
1998
+ reason: `${code}:migration_state_unsafe`,
1999
+ message: `one-time standalone migration failed (${code}:migration_state_unsafe:${startupState.detail})`,
2000
+ requiresForegroundExit: true,
2001
+ };
2002
+ }
2003
+ const hadPriorAmbiguity = startupState.classification === 'retryable'
2004
+ || (!startupState.canonicalPresent && unresolvedLegacyMigrationOwnership(env, readText));
2005
+ let ownerLeaseHeld = false;
2006
+ // A prior active canonical record remains the recovery authority until this owner publishes
2007
+ // a new authoritative install/registration transition. Advisory progress/failure writes must
2008
+ // never erase the only durable evidence of possible artifact or child ownership.
2009
+ let canonicalMutationStateActive = startupState.classification === 'retryable';
149
2010
  const skipped = (reason, message) => {
150
- writeMigrationState(env, { state: 'skipped', reason }, options);
2011
+ if (hadPriorAmbiguity) {
2012
+ return {
2013
+ outcome: 'failed',
2014
+ reason: `${SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED}:ambiguous_pending:${reason}`,
2015
+ message: 'one-time standalone migration cannot skip while registration ownership is unresolved',
2016
+ requiresForegroundExit: true,
2017
+ };
2018
+ }
151
2019
  return { outcome: 'skipped', reason, message };
152
2020
  };
153
2021
  // Gate order: switch → root → CI → container → cooldown. shouldBootstrap returns
@@ -168,9 +2036,57 @@ export async function migrateToStandaloneBinary(env, platform = process.platform
168
2036
  if (platform === 'linux' && looksLikeContainer(exists, readText)) {
169
2037
  return skipped('container_environment', 'one-time standalone migration skipped in a container');
170
2038
  }
171
- if (recentBootstrapFailure(env, readText, now)) {
2039
+ // A prior ambiguous attempt is retried through signed artifact validation and the durable
2040
+ // lifecycle reconciler. Until a registration succeeds, every new failure stays fail-closed.
2041
+ if (!hadPriorAmbiguity && recentBootstrapFailure(env, readText, now)) {
172
2042
  return skipped('cooldown', 'one-time standalone migration skipped (recent bootstrap failure cooldown)');
173
2043
  }
2044
+ if (startupState.classification === 'retryable'
2045
+ && startupState.record.state === 'installing') {
2046
+ const reconciliation = await reconcileStoredMigrationInstall({
2047
+ env,
2048
+ options,
2049
+ record: startupState.record,
2050
+ expectedPayload: startupState.raw,
2051
+ stateOptions,
2052
+ });
2053
+ if (reconciliation.action === 'return')
2054
+ return reconciliation.result;
2055
+ // Recovery used the exact stored version/destination transaction and durably terminalized
2056
+ // it. Re-read the environment only after that authority has been released.
2057
+ return migrateToStandaloneBinary(env, platform, options);
2058
+ }
2059
+ if (startupState.classification === 'retryable'
2060
+ && startupState.record.state === 'rollback_pending') {
2061
+ const reconciliation = await reconcileStoredMigrationRollbackPending({
2062
+ env,
2063
+ options,
2064
+ record: startupState.record,
2065
+ expectedPayload: startupState.raw,
2066
+ stateOptions,
2067
+ });
2068
+ if (reconciliation.action === 'return')
2069
+ return reconciliation.result;
2070
+ // Cleanup is idempotently complete and terminal state is durable; only now may the current
2071
+ // version/home request replace the old transaction authority.
2072
+ return migrateToStandaloneBinary(env, platform, options);
2073
+ }
2074
+ if (startupState.classification === 'retryable'
2075
+ && isCanonicalMigrationRegistrationState(startupState.record)) {
2076
+ const reconciliation = await reconcileStoredMigrationRegistration({
2077
+ env,
2078
+ platform,
2079
+ options,
2080
+ record: startupState.record,
2081
+ expectedPayload: startupState.raw,
2082
+ stateOptions,
2083
+ });
2084
+ if (reconciliation.action === 'return')
2085
+ return reconciliation.result;
2086
+ // The exact old transaction is now durably rolled back. Re-read canonical state before
2087
+ // beginning the current version/home request so no stale in-memory authority crosses it.
2088
+ return migrateToStandaloneBinary(env, platform, options);
2089
+ }
174
2090
  // RESOLVE: version, asset name, dest path.
175
2091
  const version = resolveMigrationVersion(env);
176
2092
  if (!version) {
@@ -186,130 +2102,571 @@ export async function migrateToStandaloneBinary(env, platform = process.platform
186
2102
  // Unsupported platform (e.g. win-arm64): skip without recording a cooldown-worthy attempt.
187
2103
  return skipped('unsupported_platform', `one-time standalone migration skipped (unsupported platform ${platform}/${options.arch ?? process.arch})`);
188
2104
  }
189
- const destPath = join(resolveMigrationHome(env), 'bin', assetName);
190
- writeMigrationState(env, { state: 'in_progress', version, destPath }, options);
191
- const fail = (reason, message, attemptDetail) => {
192
- recordBootstrapAttempt(env, { ok: false, reason: 'migration_failed', detail: attemptDetail ?? reason }, recordOptions);
193
- writeMigrationState(env, { state: 'failed', version, destPath, reason }, options);
194
- return { outcome: 'failed', reason, destPath, message };
2105
+ const migrationHome = resolveMigrationHome(env);
2106
+ const destPath = join(migrationHome, 'bin', assetName);
2107
+ const fail = (reason, message, attemptDetail, requiresForegroundExit = false, preserveCanonicalState = false) => {
2108
+ const mustExit = requiresForegroundExit || hadPriorAmbiguity;
2109
+ if (ownerLeaseHeld) {
2110
+ recordBootstrapAttempt(env, {
2111
+ ok: false,
2112
+ reason: mustExit ? 'migration_ambiguous' : 'migration_failed',
2113
+ detail: attemptDetail ?? reason,
2114
+ }, recordOptions);
2115
+ if (!canonicalMutationStateActive && !preserveCanonicalState && !mustExit) {
2116
+ writeMigrationState(env, { state: 'failed', version, destPath, reason }, options);
2117
+ }
2118
+ }
2119
+ return {
2120
+ outcome: 'failed',
2121
+ reason,
2122
+ destPath,
2123
+ message,
2124
+ ...(mustExit ? { requiresForegroundExit: true } : {}),
2125
+ };
195
2126
  };
196
- const failTimeout = (reason, message, attemptDetail) => {
197
- recordBootstrapAttempt(env, { ok: false, reason: 'migration_timeout', detail: attemptDetail }, recordOptions);
198
- writeMigrationState(env, { state: 'failed', version, destPath, reason }, options);
199
- return { outcome: 'failed', reason, destPath, message };
2127
+ const failTimeout = (reason, message, attemptDetail, requiresForegroundExit = true) => {
2128
+ const mustExit = requiresForegroundExit || hadPriorAmbiguity;
2129
+ if (ownerLeaseHeld) {
2130
+ recordBootstrapAttempt(env, {
2131
+ ok: false,
2132
+ reason: mustExit ? 'migration_ambiguous' : 'migration_timeout',
2133
+ detail: attemptDetail,
2134
+ }, recordOptions);
2135
+ if (!canonicalMutationStateActive && !mustExit) {
2136
+ writeMigrationState(env, { state: 'failed', version, destPath, reason }, options);
2137
+ }
2138
+ }
2139
+ return {
2140
+ outcome: 'failed',
2141
+ reason,
2142
+ destPath,
2143
+ message,
2144
+ ...(mustExit ? { requiresForegroundExit: true } : {}),
2145
+ };
200
2146
  };
2147
+ const timeoutMs = options.timeoutMs ?? MIGRATION_REGISTER_TIMEOUT_MS;
2148
+ const transactionBudgetMs = options.transactionBudgetMs ?? MIGRATION_REGISTER_TRANSACTION_BUDGET_MS;
2149
+ if (!options.registrationRunner) {
2150
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
2151
+ return fail(`${code}:registration_runner_unavailable`, `one-time standalone migration failed (${code}:registration_runner_unavailable)`, `${code}: registration_runner_unavailable`);
2152
+ }
2153
+ if (!Number.isSafeInteger(timeoutMs)
2154
+ || !Number.isSafeInteger(transactionBudgetMs)
2155
+ || timeoutMs <= transactionBudgetMs
2156
+ || timeoutMs > MAX_TIMER_DELAY_MS
2157
+ || transactionBudgetMs <= 0) {
2158
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
2159
+ return fail(`${code}:invalid_registration_timeout_contract`, `one-time standalone migration failed (${code}:invalid_registration_timeout_contract)`, `${code}: invalid_registration_timeout_contract`);
2160
+ }
201
2161
  const readBinary = options.readBinary ?? ((path) => fsReadFile(path));
202
2162
  const writeBinary = options.writeBinary
203
- ?? ((path, content, mode) => fsWriteFile(path, content, { mode }));
2163
+ ?? ((path, content, mode) => fsWriteFile(path, content, { mode, flag: 'wx' }));
204
2164
  const mkdir = options.mkdir ?? ((path, mode) => fsMkdir(path, { recursive: true, mode }).then(() => undefined));
205
2165
  const rm = options.rm ?? ((path) => fsRm(path, { recursive: true, force: true }));
206
- const rename = options.rename ?? ((from, to) => fsRename(from, to));
2166
+ const unlink = options.unlink ?? ((path) => fsUnlink(path));
2167
+ const rmdir = options.rmdir ?? ((path) => fsRmdir(path));
207
2168
  const chmod = options.chmod ?? ((path, mode) => fsChmod(path, mode));
2169
+ const syncFile = options.syncFile ?? (async (path) => {
2170
+ const handle = await fsOpen(path, 'r+');
2171
+ try {
2172
+ await handle.sync();
2173
+ }
2174
+ finally {
2175
+ await handle.close();
2176
+ }
2177
+ });
2178
+ const syncDirectory = options.syncDirectory ?? (async (path) => {
2179
+ let handle;
2180
+ try {
2181
+ handle = await fsOpen(path, 'r');
2182
+ await handle.sync();
2183
+ }
2184
+ catch (error) {
2185
+ // Windows does not expose reliable directory fsync handles; the executable is fsynced.
2186
+ if (process.platform !== 'win32')
2187
+ throw error;
2188
+ }
2189
+ finally {
2190
+ await handle?.close();
2191
+ }
2192
+ });
208
2193
  const stat = options.stat ?? ((path) => lstat(path));
2194
+ const link = options.link ?? ((from, to) => fsLink(from, to));
2195
+ const rename = options.rename ?? ((from, to) => fsRename(from, to));
209
2196
  const preflightFn = options.preflightFn
210
2197
  ?? ((targetPath, expectedVersion) => preflightManagedStagedBinary(targetPath, expectedVersion, options.probe));
211
2198
  const downloadFn = options.downloadFn ?? downloadGithubReleaseArtifact;
212
2199
  const verifyFn = options.verifyFn
213
2200
  ?? ((manifest, downloaded, publicKey) => ops.verifySelectedManifestArtifact(manifest, downloaded, publicKey));
214
- // Fast path: an existing dest that passes the real preflight only needs registration.
215
- if (exists(destPath)) {
216
- try {
217
- await preflightFn(destPath, version);
218
- return register(env, version, destPath, options, exists, recordOptions, fail, failTimeout, writeMigrationState);
219
- }
220
- catch {
221
- // Existing binary is unusable — fall through and download a fresh copy over it.
222
- }
223
- }
224
- // DOWNLOAD: signed manifest + staged binary under tmpdir.
225
- const directive = { required_version: version };
226
- const releaseOpts = {
227
- env,
228
- platform,
229
- arch: options.arch,
230
- fetchFn: options.fetchFn,
231
- requireSignedManifest: true,
232
- };
233
- let manifest;
234
- let download;
2201
+ const assertDirectoryTrust = options.assertDirectoryTrust
2202
+ ?? ((directory) => assertTrustedMigrationDirectoryChain(directory, uid));
2203
+ const assertFileTrust = options.assertFileTrust
2204
+ ?? ((path) => assertTrustedMigrationFile(path, uid));
2205
+ // Pure contract and read-only target validation precede even lock-namespace creation.
235
2206
  try {
236
- manifest = await resolveGithubReleaseManifest(directive, releaseOpts);
237
- download = await downloadFn(version, directive, releaseOpts);
2207
+ await assertSafeExistingMigrationTargetDirectory(migrationHome, dirname(destPath), assertDirectoryTrust);
238
2208
  }
239
2209
  catch (err) {
240
- const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_DOWNLOAD_FAILED;
2210
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
241
2211
  return fail(`${code}:${errorDetail(err)}`, `one-time standalone migration failed (${code})`, `${code}: ${errorDetail(err)}`);
242
2212
  }
243
- const stagedDir = dirname(download.stagedPath);
244
- // VERIFY: ed25519 signature over the complete manifest + sha256 of the staged bytes.
245
- // Fail closed: unverified bytes are NEVER written into the user's home.
246
- const verification = verifyFn(manifest, download.artifacts, resolveSelfUpdatePublicKey(env));
247
- if (!verification.ok) {
248
- await rmSafe(rm, stagedDir);
249
- const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_VERIFY_FAILED;
250
- return fail(`${code}:${verification.reason}`, `one-time standalone migration failed (${code})`, `${code}: ${verification.reason}`);
251
- }
252
- // Preflight the staged binary for real (`--version` + `proxy --help`) before install.
2213
+ const migrationLockDirectory = resolveBootstrapStateDir(env);
253
2214
  try {
254
- await preflightFn(download.stagedPath, version);
2215
+ await ensureSafeMigrationDirectory(migrationLockDirectory, mkdir, assertDirectoryTrust);
255
2216
  }
256
2217
  catch (err) {
257
- await rmSafe(rm, stagedDir);
258
- const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_VERIFY_FAILED;
259
- const detail = `preflight:${errorDetail(err)}`;
260
- return fail(`${code}:${detail}`, `one-time standalone migration failed (${code})`, `${code}: ${detail}`);
261
- }
262
- // INSTALL: atomic copy into <home>/bin (verified bytes only).
263
- try {
264
- await mkdir(dirname(destPath), 0o700);
265
- const stagedStat = await stat(download.stagedPath);
266
- if (stagedStat.isSymbolicLink() || !stagedStat.isFile()) {
267
- throw new Error('staged_artifact_not_regular_file');
268
- }
269
- const bytes = await readBinary(download.stagedPath);
270
- const tmpDest = `${destPath}.tmp`;
271
- try {
272
- await writeBinary(tmpDest, bytes, 0o755);
273
- await chmod(tmpDest, 0o755);
274
- await rename(tmpDest, destPath);
275
- }
276
- catch (err) {
277
- await rmSafe(rm, tmpDest);
278
- throw err;
279
- }
280
- }
281
- catch (err) {
282
- await rmSafe(rm, stagedDir);
283
2218
  const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
284
2219
  return fail(`${code}:${errorDetail(err)}`, `one-time standalone migration failed (${code})`, `${code}: ${errorDetail(err)}`);
285
2220
  }
286
- finally {
287
- await rmSafe(rm, stagedDir);
2221
+ try {
2222
+ return await withMigrationLock(migrationLockDirectory, async (assertOwnerLeaseCurrent) => {
2223
+ ownerLeaseHeld = true;
2224
+ try {
2225
+ const recoveredInstall = await recoverPendingMigrationInstall({
2226
+ env,
2227
+ stateOptions,
2228
+ readBinary,
2229
+ rename,
2230
+ link,
2231
+ unlink,
2232
+ rmdir,
2233
+ syncDirectory,
2234
+ assertDirectoryTrust,
2235
+ assertFileTrust,
2236
+ assertOwnerCurrent: assertOwnerLeaseCurrent,
2237
+ markCanonicalStateActive: () => {
2238
+ canonicalMutationStateActive = true;
2239
+ },
2240
+ });
2241
+ if (recoveredInstall)
2242
+ canonicalMutationStateActive = true;
2243
+ }
2244
+ catch (error) {
2245
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
2246
+ return fail(`${code}:recovery_failed`, `one-time standalone migration failed (${code}:recovery_failed)`, `${code}: recovery_failed:${errorDetail(error)}`, true);
2247
+ }
2248
+ // Revalidate after acquiring ownership so the read-before-mutation check cannot go stale.
2249
+ try {
2250
+ await assertSafeExistingMigrationTargetDirectory(migrationHome, dirname(destPath), assertDirectoryTrust);
2251
+ }
2252
+ catch (err) {
2253
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
2254
+ return fail(`${code}:${errorDetail(err)}`, `one-time standalone migration failed (${code})`, `${code}: ${errorDetail(err)}`);
2255
+ }
2256
+ if (!canonicalMutationStateActive) {
2257
+ writeMigrationState(env, { state: 'in_progress', version, destPath }, options);
2258
+ }
2259
+ // DOWNLOAD: signed manifest + staged binary under tmpdir.
2260
+ const directive = { required_version: version };
2261
+ const releaseOpts = {
2262
+ env,
2263
+ platform,
2264
+ arch: options.arch,
2265
+ fetchFn: options.fetchFn,
2266
+ requireSignedManifest: true,
2267
+ };
2268
+ let manifest;
2269
+ let download;
2270
+ try {
2271
+ manifest = await resolveGithubReleaseManifest(directive, releaseOpts);
2272
+ download = await downloadFn(version, directive, releaseOpts);
2273
+ }
2274
+ catch (err) {
2275
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_DOWNLOAD_FAILED;
2276
+ return fail(`${code}:${errorDetail(err)}`, `one-time standalone migration failed (${code})`, `${code}: ${errorDetail(err)}`);
2277
+ }
2278
+ const stagedDir = dirname(download.stagedPath);
2279
+ // VERIFY: ed25519 signature over the complete manifest + sha256 of the staged bytes.
2280
+ // Fail closed: unverified bytes are NEVER written into the user's home.
2281
+ const verification = verifyFn(manifest, download.artifacts, resolveSelfUpdatePublicKey(env));
2282
+ if (!verification.ok) {
2283
+ await rmSafe(rm, stagedDir);
2284
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_VERIFY_FAILED;
2285
+ const detail = errorDetail(verification.reason);
2286
+ return fail(`${code}:${detail}`, `one-time standalone migration failed (${code})`, `${code}: ${detail}`);
2287
+ }
2288
+ let stagedBytes;
2289
+ let expectedSha256;
2290
+ try {
2291
+ const stagedStat = await stat(download.stagedPath);
2292
+ if (stagedStat.isSymbolicLink() || !stagedStat.isFile()) {
2293
+ throw new Error('staged_artifact_not_regular_file');
2294
+ }
2295
+ stagedBytes = await readBinary(download.stagedPath);
2296
+ const selected = download.artifacts.length === 1 && download.artifacts[0]?.path === assetName
2297
+ ? download.artifacts[0]
2298
+ : undefined;
2299
+ expectedSha256 = selected?.sha256 ?? (selected?.bytes ? sha256(selected.bytes) : '');
2300
+ if (!/^[0-9a-f]{64}$/.test(expectedSha256) || sha256(stagedBytes) !== expectedSha256) {
2301
+ throw new Error('staged_artifact_hash_mismatch');
2302
+ }
2303
+ // Preflight the staged binary for real (`--version` + `proxy --help`) before install.
2304
+ await preflightFn(download.stagedPath, version);
2305
+ }
2306
+ catch (err) {
2307
+ await rmSafe(rm, stagedDir);
2308
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_VERIFY_FAILED;
2309
+ const detail = `preflight:${errorDetail(err)}`;
2310
+ return fail(`${code}:${detail}`, `one-time standalone migration failed (${code})`, `${code}: ${detail}`);
2311
+ }
2312
+ await rmSafe(rm, stagedDir);
2313
+ // The same lease spans INSTALL -> identity revalidation -> REGISTER -> conditional cleanup.
2314
+ // A second process must never register the target while this owner can still roll it back.
2315
+ let installResult;
2316
+ let installAnchor;
2317
+ try {
2318
+ installResult = await installVerifiedMigrationBinary({
2319
+ home: migrationHome,
2320
+ destPath,
2321
+ version,
2322
+ bytes: stagedBytes,
2323
+ expectedSha256,
2324
+ mkdir,
2325
+ writeBinary,
2326
+ readBinary,
2327
+ chmod,
2328
+ syncFile,
2329
+ syncDirectory,
2330
+ stat,
2331
+ link,
2332
+ rename,
2333
+ unlink,
2334
+ rmdir,
2335
+ preflight: preflightFn,
2336
+ assertDirectoryTrust,
2337
+ assertFileTrust,
2338
+ publishInstallIntent: async (anchor) => {
2339
+ const previousAnchor = installAnchor;
2340
+ const installingRecord = {
2341
+ state: 'installing',
2342
+ version,
2343
+ destPath,
2344
+ installAnchor: anchor,
2345
+ };
2346
+ const expectedPayload = serializeMigrationState(installingRecord, recordOptions);
2347
+ assertOwnerLeaseCurrent();
2348
+ try {
2349
+ persistAuthoritativeMigrationState(env, installingRecord, stateOptions);
2350
+ installAnchor = anchor;
2351
+ canonicalMutationStateActive = true;
2352
+ }
2353
+ catch (error) {
2354
+ try {
2355
+ const expectedPublished = readTrustedMigrationStateText(join(resolveBootstrapStateDir(env), MIGRATION_STATE_FILE), stateOptions) === expectedPayload;
2356
+ if (expectedPublished) {
2357
+ canonicalMutationStateActive = true;
2358
+ installAnchor = anchor;
2359
+ }
2360
+ else if (previousAnchor) {
2361
+ canonicalMutationStateActive = true;
2362
+ installAnchor = previousAnchor;
2363
+ }
2364
+ else {
2365
+ canonicalMutationStateActive = false;
2366
+ }
2367
+ }
2368
+ catch {
2369
+ canonicalMutationStateActive = true;
2370
+ installAnchor = previousAnchor ?? anchor;
2371
+ }
2372
+ throw error;
2373
+ }
2374
+ },
2375
+ });
2376
+ }
2377
+ catch (err) {
2378
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
2379
+ const detail = errorDetail(err);
2380
+ let ambiguous = detail.startsWith('migration_install_rollback_unconfirmed:');
2381
+ let rollbackTerminalized = false;
2382
+ if (canonicalMutationStateActive && installAnchor && !ambiguous) {
2383
+ try {
2384
+ assertOwnerLeaseCurrent();
2385
+ persistAuthoritativeMigrationState(env, { state: 'rolled_back', version, destPath, installAnchor, reason: detail }, stateOptions);
2386
+ rollbackTerminalized = true;
2387
+ }
2388
+ catch {
2389
+ ambiguous = true;
2390
+ }
2391
+ }
2392
+ return fail(`${code}:${detail}`, `one-time standalone migration failed (${code})`, `${code}: ${detail}`, ambiguous || (canonicalMutationStateActive && !rollbackTerminalized));
2393
+ }
2394
+ let registrationIdentity;
2395
+ try {
2396
+ const currentIdentity = await inspectExistingMigrationTarget(destPath, version, preflightFn, readBinary, { sha256: expectedSha256, size: stagedBytes.byteLength }, assertDirectoryTrust, assertFileTrust);
2397
+ if (!currentIdentity || !sameMigrationTargetIdentity(currentIdentity, installResult.targetIdentity)) {
2398
+ throw new Error('migration_target_identity_changed_before_registration');
2399
+ }
2400
+ registrationIdentity = currentIdentity;
2401
+ }
2402
+ catch (error) {
2403
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
2404
+ return fail(`${code}:target_identity_changed`, `one-time standalone migration failed (${code}:target_identity_changed)`, `${code}: target_identity_changed:${errorDetail(error)}`, true);
2405
+ }
2406
+ // REGISTER: delegate to the ordinary lifecycle bootstrap transaction runner.
2407
+ const sealedRegistrationIdentity = Object.freeze({ ...registrationIdentity });
2408
+ const revalidateTarget = () => assertMigrationTargetIdentityCurrent(sealedRegistrationIdentity, version, { readBinary, preflightFn, assertDirectoryTrust, assertFileTrust });
2409
+ const intentId = randomUUID();
2410
+ const intentBase = {
2411
+ version,
2412
+ destPath,
2413
+ intentId,
2414
+ targetIdentity: sealedRegistrationIdentity,
2415
+ installOwnership: {
2416
+ disposition: installResult.disposition,
2417
+ preimage: installResult.disposition === 'installed' ? 'absent' : 'matching_target',
2418
+ directoryCreated: installResult.directoryCreated,
2419
+ },
2420
+ };
2421
+ const registeringRecord = { state: 'registering', ...intentBase };
2422
+ const expectedIntentPayload = serializeMigrationState(registeringRecord, recordOptions);
2423
+ let registrationIntentPayload;
2424
+ try {
2425
+ assertOwnerLeaseCurrent();
2426
+ registrationIntentPayload = persistAuthoritativeMigrationState(env, registeringRecord, stateOptions);
2427
+ canonicalMutationStateActive = true;
2428
+ }
2429
+ catch (error) {
2430
+ let intentRollbackTerminalized = false;
2431
+ let intentAuthorityAmbiguous = false;
2432
+ let currentAuthoritativePayload;
2433
+ try {
2434
+ currentAuthoritativePayload = readTrustedMigrationStateText(join(resolveBootstrapStateDir(env), MIGRATION_STATE_FILE), stateOptions);
2435
+ }
2436
+ catch {
2437
+ canonicalMutationStateActive = true;
2438
+ intentAuthorityAmbiguous = true;
2439
+ }
2440
+ if (!intentAuthorityAmbiguous && currentAuthoritativePayload === expectedIntentPayload) {
2441
+ canonicalMutationStateActive = true;
2442
+ try {
2443
+ assertOwnerLeaseCurrent();
2444
+ const rollbackPendingPayload = persistAuthoritativeMigrationState(env, { state: 'rollback_pending', ...intentBase, reason: 'intent_persistence_failed' }, stateOptions);
2445
+ await completeMigrationRollbackPending({
2446
+ env,
2447
+ record: { ...intentBase, reason: 'intent_persistence_failed' },
2448
+ expectedPayload: rollbackPendingPayload,
2449
+ stateOptions,
2450
+ readBinary,
2451
+ preflightFn,
2452
+ rename,
2453
+ link,
2454
+ unlink,
2455
+ rmdir,
2456
+ syncDirectory,
2457
+ assertDirectoryTrust,
2458
+ assertFileTrust,
2459
+ assertOwnerCurrent: assertOwnerLeaseCurrent,
2460
+ });
2461
+ intentRollbackTerminalized = true;
2462
+ }
2463
+ catch (cleanupError) {
2464
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
2465
+ return fail(`${code}:cleanup_failed`, `one-time standalone migration failed (${code}:cleanup_failed)`, `${code}: cleanup_failed:${errorDetail(cleanupError)}`, true, true);
2466
+ }
2467
+ }
2468
+ else if (!intentAuthorityAmbiguous && currentAuthoritativePayload !== undefined) {
2469
+ const currentRecord = parseCanonicalMigrationState(currentAuthoritativePayload);
2470
+ if (currentRecord?.state !== 'installing'
2471
+ || currentRecord.version !== version
2472
+ || currentRecord.destPath !== destPath
2473
+ || !installAnchor
2474
+ || !sameMigrationInstallAnchor(currentRecord.installAnchor, installAnchor)) {
2475
+ canonicalMutationStateActive = true;
2476
+ intentAuthorityAmbiguous = true;
2477
+ }
2478
+ else {
2479
+ canonicalMutationStateActive = true;
2480
+ try {
2481
+ const recovered = await recoverPendingMigrationInstall({
2482
+ env,
2483
+ stateOptions,
2484
+ readBinary,
2485
+ rename,
2486
+ link,
2487
+ unlink,
2488
+ rmdir,
2489
+ syncDirectory,
2490
+ assertDirectoryTrust,
2491
+ assertFileTrust,
2492
+ assertOwnerCurrent: assertOwnerLeaseCurrent,
2493
+ markCanonicalStateActive: () => {
2494
+ canonicalMutationStateActive = true;
2495
+ },
2496
+ });
2497
+ if (!recovered)
2498
+ throw new Error('migration_install_journal_changed');
2499
+ intentRollbackTerminalized = true;
2500
+ }
2501
+ catch (cleanupError) {
2502
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
2503
+ return fail(`${code}:cleanup_failed`, `one-time standalone migration failed (${code}:cleanup_failed)`, `${code}: cleanup_failed:${errorDetail(cleanupError)}`, true, true);
2504
+ }
2505
+ }
2506
+ }
2507
+ else if (!intentAuthorityAmbiguous) {
2508
+ // The installed artifact cannot be deleted without a durable transaction record.
2509
+ canonicalMutationStateActive = true;
2510
+ intentAuthorityAmbiguous = true;
2511
+ }
2512
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
2513
+ return fail(`${code}:intent_persistence_failed`, `one-time standalone migration failed (${code}:intent_persistence_failed)`, `${code}: intent_persistence_failed:${errorDetail(error)}`, intentAuthorityAmbiguous || !intentRollbackTerminalized, true);
2514
+ }
2515
+ const assertRegistrationIntentCurrent = async () => {
2516
+ assertOwnerLeaseCurrent();
2517
+ assertAuthoritativeMigrationStateCurrent(env, registrationIntentPayload, stateOptions);
2518
+ };
2519
+ const registration = await register(env, platform, version, destPath, sealedRegistrationIdentity, revalidateTarget, assertRegistrationIntentCurrent, options, timeoutMs, transactionBudgetMs, fail, failTimeout);
2520
+ if (registration.outcome === 'failed' && registration.requiresForegroundExit === true) {
2521
+ try {
2522
+ assertOwnerLeaseCurrent();
2523
+ persistAuthoritativeMigrationState(env, { state: 'blocked', ...intentBase, reason: registration.reason }, stateOptions);
2524
+ }
2525
+ catch {
2526
+ // Preserve the already durable registering intent if blocked-state publication fails.
2527
+ }
2528
+ return registration;
2529
+ }
2530
+ if (registration.outcome === 'failed') {
2531
+ try {
2532
+ await assertRegistrationIntentCurrent();
2533
+ const rollbackPendingPayload = persistAuthoritativeMigrationState(env, { state: 'rollback_pending', ...intentBase, reason: registration.reason }, stateOptions);
2534
+ canonicalMutationStateActive = true;
2535
+ await completeMigrationRollbackPending({
2536
+ env,
2537
+ record: { ...intentBase, reason: registration.reason },
2538
+ expectedPayload: rollbackPendingPayload,
2539
+ stateOptions,
2540
+ readBinary,
2541
+ preflightFn,
2542
+ rename,
2543
+ link,
2544
+ unlink,
2545
+ rmdir,
2546
+ syncDirectory,
2547
+ assertDirectoryTrust,
2548
+ assertFileTrust,
2549
+ assertOwnerCurrent: assertOwnerLeaseCurrent,
2550
+ });
2551
+ canonicalMutationStateActive = false;
2552
+ }
2553
+ catch (error) {
2554
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
2555
+ return fail(`${code}:cleanup_failed`, `one-time standalone migration failed (${code}:cleanup_failed)`, `${code}: cleanup_failed:${errorDetail(error)}`, true);
2556
+ }
2557
+ return registration;
2558
+ }
2559
+ try {
2560
+ assertOwnerLeaseCurrent();
2561
+ persistAuthoritativeMigrationState(env, { state: 'committed', ...intentBase }, stateOptions);
2562
+ canonicalMutationStateActive = false;
2563
+ }
2564
+ catch (error) {
2565
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
2566
+ return fail(`${code}:commit_persistence_failed`, `one-time standalone migration failed (${code}:commit_persistence_failed)`, `${code}: commit_persistence_failed:${errorDetail(error)}`, true);
2567
+ }
2568
+ recordBootstrapAttempt(env, { ok: true, reason: 'migrated', detail: destPath }, recordOptions);
2569
+ return registration;
2570
+ });
2571
+ }
2572
+ catch (error) {
2573
+ if (error instanceof Error && error.message === 'migration_lock_busy') {
2574
+ return {
2575
+ outcome: 'failed',
2576
+ reason: `${SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED}:migration_in_progress`,
2577
+ destPath,
2578
+ message: 'one-time standalone migration is already owned by another live process',
2579
+ requiresForegroundExit: true,
2580
+ };
2581
+ }
2582
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
2583
+ return fail(`${code}:owner_lease_failed`, `one-time standalone migration failed (${code}:owner_lease_failed)`, `${code}: owner_lease_failed:${errorDetail(error)}`, true, true);
288
2584
  }
289
- // REGISTER: hand the installed binary to `evolver lifecycle bootstrap`.
290
- return register(env, version, destPath, options, exists, recordOptions, fail, failTimeout, writeMigrationState);
291
2585
  }
292
- async function register(env, version, destPath, options, exists, recordOptions, fail, failTimeout, writeState) {
2586
+ async function register(env, platform, version, destPath, targetIdentity, revalidateTarget, assertRegistrationIntentCurrent, options, timeoutMs, transactionBudgetMs, fail, failTimeout) {
293
2587
  const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
294
- const registration = await registerMigratedBinary(env, destPath, options, exists);
295
- if (registration.reason === 'cli_not_found') {
296
- return fail(`${code}:cli_not_found`, `one-time standalone migration failed (${code}:cli_not_found)`, `${code}: cli_not_found`);
2588
+ const runner = options.registrationRunner;
2589
+ if (!runner) {
2590
+ return fail(`${code}:registration_runner_unavailable`, `one-time standalone migration failed (${code}:registration_runner_unavailable)`, `${code}: registration_runner_unavailable`);
2591
+ }
2592
+ let startedAtMs;
2593
+ try {
2594
+ startedAtMs = (options.clock ?? Date.now)();
2595
+ }
2596
+ catch (error) {
2597
+ return fail(`${code}:registration_clock_failed`, `one-time standalone migration failed (${code}:registration_clock_failed)`, `${code}: registration_clock_failed:${errorDetail(error)}`);
297
2598
  }
298
- if (registration.reason === 'timeout') {
299
- return failTimeout('migration_register_timeout', `one-time standalone migration failed (${code}:timeout)`, `${code}: timeout`);
2599
+ const transactionDeadlineMs = startedAtMs + transactionBudgetMs;
2600
+ const parentDeadlineMs = startedAtMs + timeoutMs;
2601
+ if (!Number.isSafeInteger(startedAtMs)
2602
+ || startedAtMs < 0
2603
+ || !Number.isSafeInteger(transactionDeadlineMs)
2604
+ || !Number.isSafeInteger(parentDeadlineMs)) {
2605
+ return fail(`${code}:invalid_registration_deadline`, `one-time standalone migration failed (${code}:invalid_registration_deadline)`, `${code}: invalid_registration_deadline`);
300
2606
  }
2607
+ const request = Object.freeze({
2608
+ env: Object.freeze({ ...env, EVOLVER_SELF_UPDATE_TARGET_PATH: destPath }),
2609
+ platform,
2610
+ version,
2611
+ startedAtMs,
2612
+ transactionDeadlineMs,
2613
+ parentDeadlineMs,
2614
+ transactionBudgetMs,
2615
+ timeoutMs,
2616
+ targetIdentity,
2617
+ revalidateTarget,
2618
+ assertRegistrationIntentCurrent,
2619
+ // Registration must execute the verified standalone binary, never the npm/JS Node entry.
2620
+ execPath: destPath,
2621
+ ...(options.exists !== undefined ? { exists: options.exists } : {}),
2622
+ ...(options.readFile !== undefined ? { readFile: options.readFile } : {}),
2623
+ ...(options.writeFile !== undefined ? { writeFile: options.writeFile } : {}),
2624
+ ...(options.spawnFn !== undefined ? { spawnFn: options.spawnFn } : {}),
2625
+ });
2626
+ let registration;
2627
+ try {
2628
+ registration = await runner(request);
2629
+ }
2630
+ catch (error) {
2631
+ return fail(`${code}:runner_threw`, `one-time standalone migration failed (${code}:runner_threw)`, errorDetail(`${code}: runner_threw:${errorDetail(error)}`), true);
2632
+ }
2633
+ if (!registration
2634
+ || typeof registration.ok !== 'boolean'
2635
+ || typeof registration.reason !== 'string'
2636
+ || registrationReason(registration.reason) === undefined
2637
+ || (registration.detail !== undefined && typeof registration.detail !== 'string')
2638
+ || (registration.requiresForegroundExit !== undefined && registration.requiresForegroundExit !== true)) {
2639
+ return fail(`${code}:invalid_reconciliation_result`, `one-time standalone migration failed (${code}:invalid_reconciliation_result)`, `${code}: invalid_reconciliation_result`, true);
2640
+ }
2641
+ const safeReason = registrationReason(registration.reason);
2642
+ if (!safeReason) {
2643
+ return fail(`${code}:invalid_reconciliation_result`, `one-time standalone migration failed (${code}:invalid_reconciliation_result)`, `${code}: invalid_reconciliation_result`, true);
2644
+ }
2645
+ const safeDetail = registration.detail === undefined ? undefined : errorDetail(registration.detail);
301
2646
  if (!registration.ok) {
302
- const detail = registration.detail ?? 'unknown';
303
- return fail(`${code}:${detail}`, `one-time standalone migration failed (${code})`, `${code}: ${detail}`);
2647
+ const ownershipAmbiguous = registration.requiresForegroundExit === true
2648
+ || ['ambiguous', 'blocked', 'signal', 'termination_unconfirmed', 'timeout'].includes(safeReason);
2649
+ const detail = safeDetail ? `:${safeDetail}` : '';
2650
+ if (safeReason === 'timeout') {
2651
+ return failTimeout('migration_register_timeout', `one-time standalone migration failed (${code}:timeout)`, errorDetail(`${code}: timeout${detail}`), true);
2652
+ }
2653
+ return fail(`${code}:${safeReason}`, `one-time standalone migration failed (${code}:${safeReason})`, errorDetail(`${code}: ${safeReason}${detail}`), ownershipAmbiguous);
2654
+ }
2655
+ const lockReleaseUnconfirmed = safeReason === 'bootstrapped_lock_release_unconfirmed';
2656
+ if ((safeReason !== 'bootstrapped' && !lockReleaseUnconfirmed)
2657
+ || (lockReleaseUnconfirmed && !safeDetail)
2658
+ || registration.requiresForegroundExit === true) {
2659
+ return fail(`${code}:invalid_success_reconciliation`, `one-time standalone migration failed (${code}:invalid_success_reconciliation)`, `${code}: invalid_success_reconciliation:${safeReason}`, true);
304
2660
  }
305
- recordBootstrapAttempt(env, { ok: true, reason: 'migrated', detail: destPath }, recordOptions);
306
- writeState(env, { state: 'migrated', version, destPath }, options);
307
2661
  return {
308
2662
  outcome: 'migrated',
309
2663
  reason: 'migrated',
310
2664
  destPath,
311
2665
  message: `[evolver-proxy] self-update: installed standalone binary ${version} at ${destPath} and registered `
312
2666
  + 'durable service supervision via `evolver lifecycle bootstrap`; handing over to the service manager '
313
- + 'and exiting so it can take the IPC port.',
2667
+ + 'and exiting so it can take the IPC port.'
2668
+ + (lockReleaseUnconfirmed
2669
+ ? ` The service commit is durable, but lifecycle lock release remains unconfirmed (${safeDetail}).`
2670
+ : ''),
314
2671
  };
315
2672
  }