@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.
- package/dist/bin/evolver-llm-proxy.js +0 -0
- package/dist/bin/evolver-proxy.d.ts +105 -7
- package/dist/bin/evolver-proxy.js +877 -121
- package/dist/daemon/atpConsent.js +5 -2
- package/dist/daemon/collaborationFacade.js +26 -16
- package/dist/daemon/proxyDaemon.d.ts +65 -0
- package/dist/daemon/proxyDaemon.js +1384 -29
- package/dist/daemon/publishRecallVerifier.d.ts +114 -0
- package/dist/daemon/publishRecallVerifier.js +495 -0
- package/dist/daemon/selectHub.js +5 -3
- package/dist/daemon/systemdNotifier.d.ts +48 -0
- package/dist/daemon/systemdNotifier.js +163 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/lifecycle/claimNudge.d.ts +20 -0
- package/dist/lifecycle/claimNudge.js +124 -0
- package/dist/lifecycle/legacyNodeId.d.ts +11 -13
- package/dist/lifecycle/legacyNodeId.js +35 -20
- package/dist/lifecycle/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +15 -2
- package/dist/llm/server.js +24 -4
- package/dist/llm/traceControl.js +1 -1
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +72 -2
- package/dist/private/accountAssetCompatibility.d.ts +29 -0
- package/dist/private/accountAssetCompatibility.js +196 -0
- package/dist/private/adapterLoader.d.ts +21 -1
- package/dist/private/adapterLoader.js +242 -7
- package/dist/private/nodeCredentialStore.d.ts +23 -0
- package/dist/private/nodeCredentialStore.js +210 -0
- package/dist/router/messagesRoute.js +9 -3
- package/dist/router/providerRoutes.js +7 -3
- package/dist/selfUpdate/bootstrap.d.ts +162 -0
- package/dist/selfUpdate/bootstrap.js +3524 -0
- package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
- package/dist/selfUpdate/bootstrapReadiness.js +153 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
- package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
- package/dist/selfUpdate/executor.d.ts +27 -11
- package/dist/selfUpdate/executor.js +233 -58
- package/dist/selfUpdate/failureCodes.d.ts +10 -0
- package/dist/selfUpdate/failureCodes.js +13 -0
- package/dist/selfUpdate/index.d.ts +5 -1
- package/dist/selfUpdate/index.js +5 -1
- package/dist/selfUpdate/lastUpdate.d.ts +3 -1
- package/dist/selfUpdate/lastUpdate.js +37 -6
- package/dist/selfUpdate/migration.d.ts +158 -0
- package/dist/selfUpdate/migration.js +2672 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +76 -2
- package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
- package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
- package/dist/selfUpdate/releaseBinary.d.ts +13 -0
- package/dist/selfUpdate/releaseBinary.js +93 -10
- package/dist/selfUpdate/transaction.d.ts +117 -0
- package/dist/selfUpdate/transaction.js +1322 -0
- package/dist/selfUpdate/unixController.d.ts +23 -0
- package/dist/selfUpdate/unixController.js +514 -0
- package/dist/selfUpdate/version.d.ts +6 -2
- package/dist/selfUpdate/version.js +5 -3
- package/dist/selfUpdate/windowsController.d.ts +35 -0
- package/dist/selfUpdate/windowsController.js +655 -0
- package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
- package/dist/selfUpdate/windowsUpdater.js +882 -0
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +255 -64
- 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
|
+
}
|