@evomap/evolver-proxy 2.0.0-beta.2 → 2.0.0-beta.22

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 (69) hide show
  1. package/dist/bin/evolver-llm-proxy.js +0 -0
  2. package/dist/bin/evolver-proxy.d.ts +105 -7
  3. package/dist/bin/evolver-proxy.js +877 -121
  4. package/dist/daemon/atpConsent.js +5 -2
  5. package/dist/daemon/collaborationFacade.js +26 -16
  6. package/dist/daemon/proxyDaemon.d.ts +65 -0
  7. package/dist/daemon/proxyDaemon.js +1384 -29
  8. package/dist/daemon/publishRecallVerifier.d.ts +114 -0
  9. package/dist/daemon/publishRecallVerifier.js +495 -0
  10. package/dist/daemon/selectHub.js +5 -3
  11. package/dist/daemon/systemdNotifier.d.ts +48 -0
  12. package/dist/daemon/systemdNotifier.js +163 -0
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.js +4 -1
  15. package/dist/lifecycle/claimNudge.d.ts +20 -0
  16. package/dist/lifecycle/claimNudge.js +124 -0
  17. package/dist/lifecycle/legacyNodeId.d.ts +11 -13
  18. package/dist/lifecycle/legacyNodeId.js +35 -20
  19. package/dist/lifecycle/manager.d.ts +4 -0
  20. package/dist/lifecycle/manager.js +15 -2
  21. package/dist/llm/server.js +24 -4
  22. package/dist/llm/traceControl.js +1 -1
  23. package/dist/llm/upstream.d.ts +5 -1
  24. package/dist/llm/upstream.js +72 -2
  25. package/dist/private/accountAssetCompatibility.d.ts +29 -0
  26. package/dist/private/accountAssetCompatibility.js +196 -0
  27. package/dist/private/adapterLoader.d.ts +21 -1
  28. package/dist/private/adapterLoader.js +242 -7
  29. package/dist/private/nodeCredentialStore.d.ts +23 -0
  30. package/dist/private/nodeCredentialStore.js +210 -0
  31. package/dist/router/messagesRoute.js +9 -3
  32. package/dist/router/providerRoutes.js +7 -3
  33. package/dist/selfUpdate/bootstrap.d.ts +162 -0
  34. package/dist/selfUpdate/bootstrap.js +3524 -0
  35. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  36. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  37. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  38. package/dist/selfUpdate/builtinKey.js +16 -0
  39. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  40. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  41. package/dist/selfUpdate/executor.d.ts +27 -11
  42. package/dist/selfUpdate/executor.js +233 -58
  43. package/dist/selfUpdate/failureCodes.d.ts +10 -0
  44. package/dist/selfUpdate/failureCodes.js +13 -0
  45. package/dist/selfUpdate/index.d.ts +5 -1
  46. package/dist/selfUpdate/index.js +5 -1
  47. package/dist/selfUpdate/lastUpdate.d.ts +3 -1
  48. package/dist/selfUpdate/lastUpdate.js +37 -6
  49. package/dist/selfUpdate/migration.d.ts +158 -0
  50. package/dist/selfUpdate/migration.js +2672 -0
  51. package/dist/selfUpdate/policy.d.ts +19 -2
  52. package/dist/selfUpdate/policy.js +76 -2
  53. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  54. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  55. package/dist/selfUpdate/releaseBinary.d.ts +13 -0
  56. package/dist/selfUpdate/releaseBinary.js +93 -10
  57. package/dist/selfUpdate/transaction.d.ts +117 -0
  58. package/dist/selfUpdate/transaction.js +1322 -0
  59. package/dist/selfUpdate/unixController.d.ts +23 -0
  60. package/dist/selfUpdate/unixController.js +514 -0
  61. package/dist/selfUpdate/version.d.ts +6 -2
  62. package/dist/selfUpdate/version.js +5 -3
  63. package/dist/selfUpdate/windowsController.d.ts +35 -0
  64. package/dist/selfUpdate/windowsController.js +655 -0
  65. package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
  66. package/dist/selfUpdate/windowsUpdater.js +882 -0
  67. package/dist/sync/engine.d.ts +12 -0
  68. package/dist/sync/engine.js +255 -64
  69. package/package.json +10 -3
@@ -0,0 +1,2672 @@
1
+ // One-time npm/JS → standalone binary migration for the DEFAULT self-update policy.
2
+ //
3
+ // The npm/JS install shape has no replaceable standalone binary target, so the supervision
4
+ // bootstrap (bootstrap.ts) refuses to register a supervised instance for it (it would crash
5
+ // at self-update target resolution on every startup). Instead, the first degraded startup
6
+ // may ONCE download the signed standalone release binary for this platform into the user's
7
+ // evolver home (`<home>/bin`, mirroring the CLI-side lifecyclePaths home) and hand over:
8
+ //
9
+ // RESOLVE (version / asset name / dest path)
10
+ // DOWNLOAD (signed manifest + binary staged under tmpdir)
11
+ // VERIFY (ed25519 manifest signature + sha256 + real preflight probe)
12
+ // INSTALL (validate target ownership; publish verified bytes atomically without replacement)
13
+ // REGISTER (delegate to the lifecycle bootstrap transaction runner with absolute deadlines)
14
+ //
15
+ // Migration is a convenience, never an escalation: it is skipped when disabled
16
+ // (EVOLVER_BOOTSTRAP_MIGRATION=0|off), for root (non-win32), CI, containers, within a
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';
26
+ import { homedir } from 'node:os';
27
+ import { dirname, isAbsolute, join, parse as parsePath, relative, resolve as resolvePath, win32 } from 'node:path';
28
+ import { ops, util } from '@evomap/evolver-core';
29
+ import { SELF_UPDATE_FAILURE_CODES } from './failureCodes.js';
30
+ import { resolveSelfUpdatePublicKey } from './builtinKey.js';
31
+ import { downloadGithubReleaseArtifact, releaseAssetName, resolveGithubReleaseManifest, } from './releaseBinary.js';
32
+ import { preflightManagedStagedBinary } from './transaction.js';
33
+ import { getCurrentVersion } from './version.js';
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';
47
+ /**
48
+ * Migration install home — mirrors the CLI-side lifecyclePaths home resolution
49
+ * (kept dependency-free across packages): EVOLVER_HOME ?? EVOMAP_HOME ?? ~/.evomap.
50
+ */
51
+ export function resolveMigrationHome(env) {
52
+ return resolvePath(env['EVOLVER_HOME'] ?? env['EVOMAP_HOME'] ?? join(homedir(), '.evomap'));
53
+ }
54
+ /** Migration install path for this platform: <home>/bin/<releaseAssetName>. Throws on unsupported platforms. */
55
+ export function resolveMigrationDestPath(env, platform = process.platform, arch = process.arch) {
56
+ return join(resolveMigrationHome(env), 'bin', releaseAssetName(platform, arch));
57
+ }
58
+ /**
59
+ * Resolve the migration target version: EVOLVER_BOOTSTRAP_MIGRATION_VERSION override
60
+ * (normalized through the self-update version contract) wins, else the current package
61
+ * version. Returns undefined when nothing normalizes to a concrete semver.
62
+ */
63
+ export function resolveMigrationVersion(env) {
64
+ const override = env['EVOLVER_BOOTSTRAP_MIGRATION_VERSION']?.trim();
65
+ if (override)
66
+ return ops.normalizeRequiredVersion(override);
67
+ return ops.normalizeConcreteVersion(getCurrentVersion());
68
+ }
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
+ }
177
+ const defaultWriteTextFile = (path, content) => {
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
+ }
236
+ };
237
+ function resolveProcessUid() {
238
+ const getuid = process.getuid;
239
+ return typeof getuid === 'function' ? getuid.call(process) : undefined;
240
+ }
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
+ }
536
+ function writeMigrationState(env, record, options) {
537
+ try {
538
+ const payload = serializeMigrationState(record, options);
539
+ const write = options.writeFile ?? defaultWriteTextFile;
540
+ write(join(resolveBootstrapStateDir(env), MIGRATION_STATE_FILE), payload);
541
+ }
542
+ catch {
543
+ // Pre-registration diagnostics are advisory; registration intent is persisted separately.
544
+ }
545
+ }
546
+ async function rmSafe(rm, path) {
547
+ try {
548
+ await rm(path);
549
+ }
550
+ catch {
551
+ // Best-effort cleanup only.
552
+ }
553
+ }
554
+ function errorDetail(err) {
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) {
1253
+ try {
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
+ });
1263
+ }
1264
+ catch (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);
1297
+ return;
1298
+ }
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,
1458
+ };
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',
1480
+ });
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',
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',
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
+ }
1973
+ }
1974
+ /**
1975
+ * One-time migration of the npm/JS install shape to the standalone release binary.
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.
1978
+ */
1979
+ export async function migrateToStandaloneBinary(env, platform = process.platform, options = {}) {
1980
+ const now = options.now ?? Date.now();
1981
+ const exists = options.exists ?? existsSync;
1982
+ const readText = options.readFile ?? defaultReadTextFile;
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';
2010
+ const skipped = (reason, message) => {
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
+ }
2019
+ return { outcome: 'skipped', reason, message };
2020
+ };
2021
+ // Gate order: switch → root → CI → container → cooldown. shouldBootstrap returns
2022
+ // unsupported_install_shape BEFORE its own CI/container checks, so migration must
2023
+ // re-check every environment guard itself.
2024
+ const migrationSwitch = env['EVOLVER_BOOTSTRAP_MIGRATION']?.trim();
2025
+ if (migrationSwitch === '0' || migrationSwitch === 'off') {
2026
+ return skipped('disabled', 'one-time standalone migration disabled (EVOLVER_BOOTSTRAP_MIGRATION)');
2027
+ }
2028
+ const uid = options.uid ?? resolveProcessUid();
2029
+ if (platform !== 'win32' && uid === 0) {
2030
+ return skipped('root_user', 'one-time standalone migration skipped for root');
2031
+ }
2032
+ const ci = env['CI']?.trim();
2033
+ if (ci && ci.toLowerCase() !== 'false' && ci !== '0') {
2034
+ return skipped('ci_environment', 'one-time standalone migration skipped in CI');
2035
+ }
2036
+ if (platform === 'linux' && looksLikeContainer(exists, readText)) {
2037
+ return skipped('container_environment', 'one-time standalone migration skipped in a container');
2038
+ }
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)) {
2042
+ return skipped('cooldown', 'one-time standalone migration skipped (recent bootstrap failure cooldown)');
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
+ }
2090
+ // RESOLVE: version, asset name, dest path.
2091
+ const version = resolveMigrationVersion(env);
2092
+ if (!version) {
2093
+ const overrideSet = Boolean(env['EVOLVER_BOOTSTRAP_MIGRATION_VERSION']?.trim());
2094
+ const reason = overrideSet ? 'invalid_version_override' : 'version_unresolvable';
2095
+ return skipped(reason, `one-time standalone migration skipped (${reason})`);
2096
+ }
2097
+ let assetName;
2098
+ try {
2099
+ assetName = releaseAssetName(platform, options.arch ?? process.arch);
2100
+ }
2101
+ catch {
2102
+ // Unsupported platform (e.g. win-arm64): skip without recording a cooldown-worthy attempt.
2103
+ return skipped('unsupported_platform', `one-time standalone migration skipped (unsupported platform ${platform}/${options.arch ?? process.arch})`);
2104
+ }
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
+ };
2126
+ };
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
+ };
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
+ }
2161
+ const readBinary = options.readBinary ?? ((path) => fsReadFile(path));
2162
+ const writeBinary = options.writeBinary
2163
+ ?? ((path, content, mode) => fsWriteFile(path, content, { mode, flag: 'wx' }));
2164
+ const mkdir = options.mkdir ?? ((path, mode) => fsMkdir(path, { recursive: true, mode }).then(() => undefined));
2165
+ const rm = options.rm ?? ((path) => fsRm(path, { recursive: true, force: true }));
2166
+ const unlink = options.unlink ?? ((path) => fsUnlink(path));
2167
+ const rmdir = options.rmdir ?? ((path) => fsRmdir(path));
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
+ });
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));
2196
+ const preflightFn = options.preflightFn
2197
+ ?? ((targetPath, expectedVersion) => preflightManagedStagedBinary(targetPath, expectedVersion, options.probe));
2198
+ const downloadFn = options.downloadFn ?? downloadGithubReleaseArtifact;
2199
+ const verifyFn = options.verifyFn
2200
+ ?? ((manifest, downloaded, publicKey) => ops.verifySelectedManifestArtifact(manifest, downloaded, publicKey));
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.
2206
+ try {
2207
+ await assertSafeExistingMigrationTargetDirectory(migrationHome, dirname(destPath), assertDirectoryTrust);
2208
+ }
2209
+ catch (err) {
2210
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
2211
+ return fail(`${code}:${errorDetail(err)}`, `one-time standalone migration failed (${code})`, `${code}: ${errorDetail(err)}`);
2212
+ }
2213
+ const migrationLockDirectory = resolveBootstrapStateDir(env);
2214
+ try {
2215
+ await ensureSafeMigrationDirectory(migrationLockDirectory, mkdir, assertDirectoryTrust);
2216
+ }
2217
+ catch (err) {
2218
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_INSTALL_FAILED;
2219
+ return fail(`${code}:${errorDetail(err)}`, `one-time standalone migration failed (${code})`, `${code}: ${errorDetail(err)}`);
2220
+ }
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);
2584
+ }
2585
+ }
2586
+ async function register(env, platform, version, destPath, targetIdentity, revalidateTarget, assertRegistrationIntentCurrent, options, timeoutMs, transactionBudgetMs, fail, failTimeout) {
2587
+ const code = SELF_UPDATE_FAILURE_CODES.MIGRATION_REGISTER_FAILED;
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)}`);
2598
+ }
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`);
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);
2646
+ if (!registration.ok) {
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);
2660
+ }
2661
+ return {
2662
+ outcome: 'migrated',
2663
+ reason: 'migrated',
2664
+ destPath,
2665
+ message: `[evolver-proxy] self-update: installed standalone binary ${version} at ${destPath} and registered `
2666
+ + 'durable service supervision via `evolver lifecycle bootstrap`; handing over to the service manager '
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
+ : ''),
2671
+ };
2672
+ }