docdex 0.1.10 → 0.2.1
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/CHANGELOG.md +3 -0
- package/README.md +103 -65
- package/bin/docdex.js +145 -5
- package/lib/daemon_version.js +80 -0
- package/lib/install.js +1940 -28
- package/lib/installer_logging.js +134 -0
- package/lib/platform.js +275 -20
- package/lib/platform_matrix.js +127 -0
- package/lib/postinstall_setup.js +885 -0
- package/lib/release_manifest.js +226 -0
- package/lib/release_signing.js +93 -0
- package/package.json +4 -2
package/lib/install.js
CHANGED
|
@@ -2,18 +2,158 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const fs = require("node:fs");
|
|
5
|
+
const http = require("node:http");
|
|
5
6
|
const https = require("node:https");
|
|
6
7
|
const os = require("node:os");
|
|
7
8
|
const path = require("node:path");
|
|
8
9
|
const { pipeline } = require("node:stream/promises");
|
|
9
|
-
const
|
|
10
|
+
const crypto = require("node:crypto");
|
|
10
11
|
|
|
11
12
|
const pkg = require("../package.json");
|
|
12
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
artifactName,
|
|
15
|
+
assetPatternForPlatformKey,
|
|
16
|
+
detectPlatformKey,
|
|
17
|
+
resolvePlatformPolicy,
|
|
18
|
+
targetTripleForPlatformKey,
|
|
19
|
+
UnsupportedPlatformError
|
|
20
|
+
} = require("./platform");
|
|
21
|
+
const { ManifestResolutionError, resolveCanonicalAssetForTargetTriple } = require("./release_manifest");
|
|
22
|
+
const { runPostInstallSetup } = require("./postinstall_setup");
|
|
13
23
|
|
|
14
24
|
const MAX_REDIRECTS = 5;
|
|
15
25
|
const USER_AGENT = "docdex-installer";
|
|
16
26
|
const PLACEHOLDER_REPO_TOKEN = /OWNER|REPO/i;
|
|
27
|
+
const MAX_MANIFEST_BYTES = 1024 * 1024; // 1 MiB cap for safety
|
|
28
|
+
const INVALID_JSON_ERROR = "invalid JSON";
|
|
29
|
+
const INSTALL_METADATA_SCHEMA_VERSION = 1;
|
|
30
|
+
const INSTALL_METADATA_FILENAME = "docdexd-install.json";
|
|
31
|
+
const LEGACY_STAGING_SUFFIX = ".__docdexd_install_staging";
|
|
32
|
+
const LEGACY_BACKUP_SUFFIX = ".__docdexd_install_backup";
|
|
33
|
+
const LEGACY_INCOMING_SUFFIX = ".incoming";
|
|
34
|
+
const LEGACY_BACKUP_SIMPLE_SUFFIX = ".backup";
|
|
35
|
+
const INSTALLER_EVENT_PREFIX = "[docdex] event ";
|
|
36
|
+
const DEFAULT_INTEGRITY_CONFIG = Object.freeze({
|
|
37
|
+
metadataSources: ["manifest", "checksums", "sidecar"],
|
|
38
|
+
missingPolicy: "fallback"
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const EXIT_CODE_BY_ERROR_CODE = Object.freeze({
|
|
42
|
+
DOCDEX_INSTALLER_CONFIG: 2,
|
|
43
|
+
DOCDEX_UNSUPPORTED_PLATFORM: 3,
|
|
44
|
+
DOCDEX_MANIFEST_MALFORMED: 10,
|
|
45
|
+
DOCDEX_TARGET_TRIPLE_INVALID: 11,
|
|
46
|
+
DOCDEX_ASSET_NO_MATCH: 12,
|
|
47
|
+
DOCDEX_ASSET_MULTI_MATCH: 13,
|
|
48
|
+
DOCDEX_ASSET_MALFORMED: 14,
|
|
49
|
+
DOCDEX_CHECKSUM_UNUSABLE: 24,
|
|
50
|
+
DOCDEX_DOWNLOAD_FAILED: 20,
|
|
51
|
+
DOCDEX_ASSET_MISSING: 21,
|
|
52
|
+
DOCDEX_INTEGRITY_MISMATCH: 22,
|
|
53
|
+
DOCDEX_ARCHIVE_INVALID: 23
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function withBaseDetails(details) {
|
|
57
|
+
return {
|
|
58
|
+
targetTriple: null,
|
|
59
|
+
manifestVersion: null,
|
|
60
|
+
assetName: null,
|
|
61
|
+
...(details || {})
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class InstallerConfigError extends Error {
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} message
|
|
68
|
+
* @param {object} [details]
|
|
69
|
+
*/
|
|
70
|
+
constructor(message, details) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "InstallerConfigError";
|
|
73
|
+
this.code = "DOCDEX_INSTALLER_CONFIG";
|
|
74
|
+
this.exitCode = EXIT_CODE_BY_ERROR_CODE[this.code];
|
|
75
|
+
this.details = withBaseDetails(details);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class MissingArtifactError extends Error {
|
|
80
|
+
/**
|
|
81
|
+
* @param {object} details
|
|
82
|
+
*/
|
|
83
|
+
constructor(details) {
|
|
84
|
+
super("Missing release artifact for detected platform");
|
|
85
|
+
this.name = "MissingArtifactError";
|
|
86
|
+
this.code = "DOCDEX_ASSET_MISSING";
|
|
87
|
+
this.exitCode = EXIT_CODE_BY_ERROR_CODE[this.code];
|
|
88
|
+
this.details = withBaseDetails(details);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class DownloadError extends Error {
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} message
|
|
95
|
+
* @param {object} details
|
|
96
|
+
* @param {Error} [cause]
|
|
97
|
+
*/
|
|
98
|
+
constructor(message, details, cause) {
|
|
99
|
+
super(message, cause ? { cause } : undefined);
|
|
100
|
+
this.name = "DownloadError";
|
|
101
|
+
this.code = "DOCDEX_DOWNLOAD_FAILED";
|
|
102
|
+
this.exitCode = EXIT_CODE_BY_ERROR_CODE[this.code];
|
|
103
|
+
this.details = withBaseDetails(details);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class IntegrityMismatchError extends Error {
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} archiveName
|
|
110
|
+
* @param {string} expectedSha256
|
|
111
|
+
* @param {string} actualSha256
|
|
112
|
+
* @param {object} [details]
|
|
113
|
+
*/
|
|
114
|
+
constructor(archiveName, expectedSha256, actualSha256, details) {
|
|
115
|
+
super(
|
|
116
|
+
`Integrity check failed for ${archiveName}: expected sha256=${expectedSha256} got sha256=${actualSha256}`
|
|
117
|
+
);
|
|
118
|
+
this.name = "IntegrityMismatchError";
|
|
119
|
+
this.code = "DOCDEX_INTEGRITY_MISMATCH";
|
|
120
|
+
this.exitCode = EXIT_CODE_BY_ERROR_CODE[this.code];
|
|
121
|
+
this.details = withBaseDetails({
|
|
122
|
+
...details,
|
|
123
|
+
assetName: archiveName,
|
|
124
|
+
expectedSha256,
|
|
125
|
+
actualSha256
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class ArchiveInvalidError extends Error {
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} message
|
|
133
|
+
* @param {object} details
|
|
134
|
+
*/
|
|
135
|
+
constructor(message, details) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.name = "ArchiveInvalidError";
|
|
138
|
+
this.code = "DOCDEX_ARCHIVE_INVALID";
|
|
139
|
+
this.exitCode = EXIT_CODE_BY_ERROR_CODE[this.code];
|
|
140
|
+
this.details = withBaseDetails(details);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
class ChecksumResolutionError extends Error {
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} message
|
|
147
|
+
* @param {object} [details]
|
|
148
|
+
*/
|
|
149
|
+
constructor(message, details) {
|
|
150
|
+
super(message);
|
|
151
|
+
this.name = "ChecksumResolutionError";
|
|
152
|
+
this.code = "DOCDEX_CHECKSUM_UNUSABLE";
|
|
153
|
+
this.exitCode = EXIT_CODE_BY_ERROR_CODE[this.code];
|
|
154
|
+
this.details = withBaseDetails(details);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
17
157
|
|
|
18
158
|
function parseRepoSlug() {
|
|
19
159
|
const envRepo = process.env.DOCDEX_DOWNLOAD_REPO;
|
|
@@ -26,7 +166,10 @@ function parseRepoSlug() {
|
|
|
26
166
|
return match[1];
|
|
27
167
|
}
|
|
28
168
|
|
|
29
|
-
throw new
|
|
169
|
+
throw new InstallerConfigError(
|
|
170
|
+
"Set DOCDEX_DOWNLOAD_REPO env var or update package.json repository.url to owner/repo",
|
|
171
|
+
{ repoSlug: null }
|
|
172
|
+
);
|
|
30
173
|
}
|
|
31
174
|
|
|
32
175
|
function getDownloadBase(repoSlug) {
|
|
@@ -38,7 +181,9 @@ function getVersion() {
|
|
|
38
181
|
const version = (envVersion || pkg.version || "").replace(/^v/, "");
|
|
39
182
|
|
|
40
183
|
if (!version) {
|
|
41
|
-
throw new
|
|
184
|
+
throw new InstallerConfigError("Missing package version; set DOCDEX_VERSION or package.json version", {
|
|
185
|
+
version: null
|
|
186
|
+
});
|
|
42
187
|
}
|
|
43
188
|
|
|
44
189
|
return version;
|
|
@@ -51,22 +196,91 @@ function requestOptions() {
|
|
|
51
196
|
return { headers };
|
|
52
197
|
}
|
|
53
198
|
|
|
199
|
+
function selectHttpClient(url) {
|
|
200
|
+
try {
|
|
201
|
+
const protocol = new URL(url).protocol;
|
|
202
|
+
if (protocol === "http:") return http;
|
|
203
|
+
if (protocol === "https:") return https;
|
|
204
|
+
} catch {
|
|
205
|
+
// Fall through to default.
|
|
206
|
+
}
|
|
207
|
+
return https;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resolveRedirectUrl(location, baseUrl) {
|
|
211
|
+
try {
|
|
212
|
+
return new URL(location, baseUrl).toString();
|
|
213
|
+
} catch {
|
|
214
|
+
return location;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function downloadText(url, redirects = 0) {
|
|
219
|
+
if (redirects > MAX_REDIRECTS) {
|
|
220
|
+
throw new Error(`Too many redirects while fetching ${url}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const client = selectHttpClient(url);
|
|
225
|
+
client
|
|
226
|
+
.get(url, requestOptions(), (res) => {
|
|
227
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
228
|
+
res.resume();
|
|
229
|
+
const redirectUrl = resolveRedirectUrl(res.headers.location, url);
|
|
230
|
+
return downloadText(redirectUrl, redirects + 1).then(resolve, reject);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (res.statusCode !== 200) {
|
|
234
|
+
res.resume();
|
|
235
|
+
const err = new Error(`Download failed (${res.statusCode}) from ${url}`);
|
|
236
|
+
err.statusCode = res.statusCode;
|
|
237
|
+
err.url = url;
|
|
238
|
+
return reject(err);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const chunks = [];
|
|
242
|
+
let total = 0;
|
|
243
|
+
res.on("data", (chunk) => {
|
|
244
|
+
total += chunk.length;
|
|
245
|
+
if (total > MAX_MANIFEST_BYTES) {
|
|
246
|
+
const err = new Error(`Response too large while fetching ${url} (>${MAX_MANIFEST_BYTES} bytes)`);
|
|
247
|
+
err.code = "DOCDEX_DOWNLOAD_TOO_LARGE";
|
|
248
|
+
err.url = url;
|
|
249
|
+
err.maxBytes = MAX_MANIFEST_BYTES;
|
|
250
|
+
err.actualBytes = total;
|
|
251
|
+
res.destroy(err);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
chunks.push(chunk);
|
|
255
|
+
});
|
|
256
|
+
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
257
|
+
res.on("error", reject);
|
|
258
|
+
})
|
|
259
|
+
.on("error", reject);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
54
263
|
function download(url, dest, redirects = 0) {
|
|
55
264
|
if (redirects > MAX_REDIRECTS) {
|
|
56
265
|
throw new Error(`Too many redirects while fetching ${url}`);
|
|
57
266
|
}
|
|
58
267
|
|
|
59
268
|
return new Promise((resolve, reject) => {
|
|
60
|
-
|
|
269
|
+
const client = selectHttpClient(url);
|
|
270
|
+
client
|
|
61
271
|
.get(url, requestOptions(), (res) => {
|
|
62
272
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
63
273
|
res.resume();
|
|
64
|
-
|
|
274
|
+
const redirectUrl = resolveRedirectUrl(res.headers.location, url);
|
|
275
|
+
return download(redirectUrl, dest, redirects + 1).then(resolve, reject);
|
|
65
276
|
}
|
|
66
277
|
|
|
67
278
|
if (res.statusCode !== 200) {
|
|
68
279
|
res.resume();
|
|
69
|
-
|
|
280
|
+
const err = new Error(`Download failed (${res.statusCode}) from ${url}`);
|
|
281
|
+
err.statusCode = res.statusCode;
|
|
282
|
+
err.url = url;
|
|
283
|
+
return reject(err);
|
|
70
284
|
}
|
|
71
285
|
|
|
72
286
|
const file = fs.createWriteStream(dest);
|
|
@@ -77,35 +291,1733 @@ function download(url, dest, redirects = 0) {
|
|
|
77
291
|
}
|
|
78
292
|
|
|
79
293
|
async function extractTarball(archivePath, targetDir) {
|
|
294
|
+
// Lazy import so unit tests can load this module without installing optional npm deps.
|
|
295
|
+
const tar = require("tar");
|
|
80
296
|
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
81
297
|
await tar.x({ file: archivePath, cwd: targetDir, gzip: true });
|
|
82
298
|
}
|
|
83
299
|
|
|
300
|
+
async function sha256File(filePath) {
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
const hash = crypto.createHash("sha256");
|
|
303
|
+
const stream = fs.createReadStream(filePath);
|
|
304
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
305
|
+
stream.on("error", reject);
|
|
306
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function installMetadataPath(distDir, pathModule = path) {
|
|
311
|
+
return pathModule.join(distDir, INSTALL_METADATA_FILENAME);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function nowIso() {
|
|
315
|
+
return new Date().toISOString();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function readJsonFileIfPossible({ fsModule, filePath }) {
|
|
319
|
+
if (!fsModule?.promises?.readFile) {
|
|
320
|
+
return { value: null, error: "readFile_unavailable", errorCode: "READFILE_UNAVAILABLE" };
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const raw = await fsModule.promises.readFile(filePath, "utf8");
|
|
324
|
+
try {
|
|
325
|
+
return { value: JSON.parse(raw), error: null, errorCode: null };
|
|
326
|
+
} catch (err) {
|
|
327
|
+
return { value: null, error: err?.message || String(err), errorCode: "INVALID_JSON" };
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return {
|
|
331
|
+
value: null,
|
|
332
|
+
error: err?.message || String(err),
|
|
333
|
+
errorCode: typeof err?.code === "string" && err.code ? err.code : "READ_ERROR"
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function writeJsonFileAtomic({ fsModule, pathModule, filePath, value }) {
|
|
339
|
+
const dir = pathModule.dirname(filePath);
|
|
340
|
+
await fsModule.promises.mkdir(dir, { recursive: true });
|
|
341
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
342
|
+
const payload = `${JSON.stringify(value, null, 2)}\n`;
|
|
343
|
+
await fsModule.promises.writeFile(tmp, payload, "utf8");
|
|
344
|
+
await fsModule.promises.rename(tmp, filePath);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isValidInstallMetadata(meta) {
|
|
348
|
+
if (!meta || typeof meta !== "object") return false;
|
|
349
|
+
if (meta.schemaVersion !== INSTALL_METADATA_SCHEMA_VERSION) return false;
|
|
350
|
+
if (typeof meta.version !== "string" || !meta.version) return false;
|
|
351
|
+
if (typeof meta.platformKey !== "string" || !meta.platformKey) return false;
|
|
352
|
+
if (!meta.binary || typeof meta.binary !== "object") return false;
|
|
353
|
+
if (typeof meta.binary.sha256 !== "string" || meta.binary.sha256.length !== 64) return false;
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function normalizeSha256Hex(value) {
|
|
358
|
+
if (typeof value !== "string") return null;
|
|
359
|
+
const trimmed = value.trim().toLowerCase();
|
|
360
|
+
if (!/^[0-9a-f]{64}$/.test(trimmed)) return null;
|
|
361
|
+
return trimmed;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function emitInstallerEvent(logger, payload) {
|
|
365
|
+
if (!logger || typeof logger.log !== "function") return;
|
|
366
|
+
try {
|
|
367
|
+
logger.log(`${INSTALLER_EVENT_PREFIX}${JSON.stringify(payload)}`);
|
|
368
|
+
} catch {
|
|
369
|
+
// Ignore telemetry failures to avoid blocking installs.
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function buildInstallNonce() {
|
|
374
|
+
const rand = Math.random().toString(16).slice(2, 10);
|
|
375
|
+
return `${Date.now()}.${process.pid}.${rand}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function stagingDownloadDir({ distBaseDir, platformKey, pathModule }) {
|
|
379
|
+
return pathModule.join(distBaseDir, ".staging", platformKey);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function stageDirName({ distDir, nonce }) {
|
|
383
|
+
return `${distDir}.stage.${nonce}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function backupDirName({ distDir, nonce }) {
|
|
387
|
+
return `${distDir}.backup.${nonce}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function failedDirName({ distDir, nonce }) {
|
|
391
|
+
return `${distDir}.failed.${nonce}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function removeDirSafe(fsModule, dirPath) {
|
|
395
|
+
if (!dirPath) return false;
|
|
396
|
+
try {
|
|
397
|
+
await fsModule.promises.rm(dirPath, { recursive: true, force: true });
|
|
398
|
+
return true;
|
|
399
|
+
} catch {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function listDirEntriesSafe(fsModule, dirPath) {
|
|
405
|
+
if (!fsModule?.promises?.readdir) return [];
|
|
406
|
+
try {
|
|
407
|
+
return await fsModule.promises.readdir(dirPath, { withFileTypes: true });
|
|
408
|
+
} catch {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function cleanupInstallArtifacts({
|
|
414
|
+
fsModule,
|
|
415
|
+
pathModule,
|
|
416
|
+
distBaseDir,
|
|
417
|
+
distDir,
|
|
418
|
+
platformKey,
|
|
419
|
+
stagingDir,
|
|
420
|
+
backupDir,
|
|
421
|
+
preserveBackups = false
|
|
422
|
+
}) {
|
|
423
|
+
const paths = new Set();
|
|
424
|
+
const cleaned = [];
|
|
425
|
+
|
|
426
|
+
if (stagingDir) paths.add(stagingDir);
|
|
427
|
+
if (backupDir && !preserveBackups) paths.add(backupDir);
|
|
428
|
+
|
|
429
|
+
if (distDir) {
|
|
430
|
+
paths.add(`${distDir}${LEGACY_STAGING_SUFFIX}`);
|
|
431
|
+
if (!preserveBackups) paths.add(`${distDir}${LEGACY_BACKUP_SUFFIX}`);
|
|
432
|
+
paths.add(`${distDir}${LEGACY_INCOMING_SUFFIX}`);
|
|
433
|
+
if (!preserveBackups) paths.add(`${distDir}${LEGACY_BACKUP_SIMPLE_SUFFIX}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (distBaseDir && platformKey) {
|
|
437
|
+
const entries = await listDirEntriesSafe(fsModule, distBaseDir);
|
|
438
|
+
for (const entry of entries) {
|
|
439
|
+
if (!entry.isDirectory()) continue;
|
|
440
|
+
const name = entry.name;
|
|
441
|
+
if (
|
|
442
|
+
name.startsWith(`${platformKey}.stage.`) ||
|
|
443
|
+
name.startsWith(`${platformKey}.staging.`) ||
|
|
444
|
+
name.startsWith(`${platformKey}.staging-`) ||
|
|
445
|
+
(!preserveBackups && name.startsWith(`${platformKey}.backup.`)) ||
|
|
446
|
+
name.startsWith(`${platformKey}.failed.`)
|
|
447
|
+
) {
|
|
448
|
+
paths.add(pathModule.join(distBaseDir, name));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const pathToRemove of paths) {
|
|
454
|
+
if (await removeDirSafe(fsModule, pathToRemove)) cleaned.push(pathToRemove);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (distBaseDir && platformKey) {
|
|
458
|
+
const downloadRoot = stagingDownloadDir({ distBaseDir, platformKey, pathModule });
|
|
459
|
+
if (await removeDirSafe(fsModule, downloadRoot)) cleaned.push(downloadRoot);
|
|
460
|
+
const stagingRoot = pathModule.join(distBaseDir, ".staging");
|
|
461
|
+
if (fsModule?.promises?.rmdir) {
|
|
462
|
+
await fsModule.promises.rmdir(stagingRoot).catch(() => {});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return cleaned;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function selectLatestCandidate(fsModule, candidates) {
|
|
470
|
+
let latest = candidates[0] || null;
|
|
471
|
+
let latestMtime = -1;
|
|
472
|
+
for (const candidate of candidates) {
|
|
473
|
+
try {
|
|
474
|
+
const stat = await fsModule.promises.stat(candidate.path);
|
|
475
|
+
const mtime = typeof stat.mtimeMs === "number" ? stat.mtimeMs : stat.mtime?.getTime?.() ?? 0;
|
|
476
|
+
if (mtime > latestMtime) {
|
|
477
|
+
latestMtime = mtime;
|
|
478
|
+
latest = candidate;
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return latest;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function recoverInterruptedInstall({ fsModule, pathModule, distDir, isWin32, logger }) {
|
|
488
|
+
const existsSync = typeof fsModule?.existsSync === "function" ? fsModule.existsSync.bind(fsModule) : null;
|
|
489
|
+
const canStat = typeof fsModule?.promises?.stat === "function";
|
|
490
|
+
const distBaseDir = pathModule.dirname(distDir);
|
|
491
|
+
const platformKey = pathModule.basename(distDir);
|
|
492
|
+
|
|
493
|
+
const backups = [];
|
|
494
|
+
const stages = [];
|
|
495
|
+
const failed = [];
|
|
496
|
+
|
|
497
|
+
const addIfExists = async (list, candidatePath, label) => {
|
|
498
|
+
if (!canStat) return;
|
|
499
|
+
try {
|
|
500
|
+
const stat = await fsModule.promises.stat(candidatePath);
|
|
501
|
+
if (stat.isDirectory()) list.push({ path: candidatePath, label });
|
|
502
|
+
} catch {
|
|
503
|
+
// ignore missing paths
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
await addIfExists(backups, `${distDir}${LEGACY_BACKUP_SUFFIX}`, "legacy_backup_suffix");
|
|
508
|
+
await addIfExists(backups, `${distDir}${LEGACY_BACKUP_SIMPLE_SUFFIX}`, "legacy_backup_simple");
|
|
509
|
+
await addIfExists(stages, `${distDir}${LEGACY_STAGING_SUFFIX}`, "legacy_staging_suffix");
|
|
510
|
+
await addIfExists(stages, `${distDir}${LEGACY_INCOMING_SUFFIX}`, "legacy_incoming");
|
|
511
|
+
|
|
512
|
+
const entries = await listDirEntriesSafe(fsModule, distBaseDir);
|
|
513
|
+
for (const entry of entries) {
|
|
514
|
+
if (!entry.isDirectory()) continue;
|
|
515
|
+
const name = entry.name;
|
|
516
|
+
const fullPath = pathModule.join(distBaseDir, name);
|
|
517
|
+
if (name.startsWith(`${platformKey}.backup.`)) {
|
|
518
|
+
backups.push({ path: fullPath, label: "backup" });
|
|
519
|
+
} else if (
|
|
520
|
+
name.startsWith(`${platformKey}.stage.`) ||
|
|
521
|
+
name.startsWith(`${platformKey}.staging.`) ||
|
|
522
|
+
name.startsWith(`${platformKey}.staging-`)
|
|
523
|
+
) {
|
|
524
|
+
stages.push({ path: fullPath, label: "stage" });
|
|
525
|
+
} else if (name.startsWith(`${platformKey}.failed.`)) {
|
|
526
|
+
failed.push({ path: fullPath, label: "failed" });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const distDirExists = existsSync ? existsSync(distDir) : false;
|
|
531
|
+
let recoveredFrom = null;
|
|
532
|
+
let action = "not_needed";
|
|
533
|
+
let recoveryError = null;
|
|
534
|
+
|
|
535
|
+
if (!distDirExists && backups.length) {
|
|
536
|
+
const candidate = await selectLatestCandidate(fsModule, backups);
|
|
537
|
+
if (candidate) {
|
|
538
|
+
try {
|
|
539
|
+
await fsModule.promises.rename(candidate.path, distDir);
|
|
540
|
+
recoveredFrom = candidate.path;
|
|
541
|
+
action = "recovered";
|
|
542
|
+
} catch (err) {
|
|
543
|
+
recoveryError = err;
|
|
544
|
+
action = "recovery_failed";
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const cleaned = [];
|
|
550
|
+
|
|
551
|
+
if (distDirExists || action === "recovered") {
|
|
552
|
+
for (const backup of backups) {
|
|
553
|
+
if (backup.path === recoveredFrom) continue;
|
|
554
|
+
if (await removeDirSafe(fsModule, backup.path)) cleaned.push(backup.path);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
for (const stage of stages) {
|
|
559
|
+
if (await removeDirSafe(fsModule, stage.path)) cleaned.push(stage.path);
|
|
560
|
+
}
|
|
561
|
+
for (const entry of failed) {
|
|
562
|
+
if (await removeDirSafe(fsModule, entry.path)) cleaned.push(entry.path);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (logger && typeof logger.log === "function" && action === "recovered") {
|
|
566
|
+
logger.log(`[docdex] recovered previous install from ${recoveredFrom}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
action,
|
|
571
|
+
recoveredFrom,
|
|
572
|
+
cleaned,
|
|
573
|
+
error: recoveryError,
|
|
574
|
+
isWin32
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function resolveIntegrityConfig(integrityConfigFn) {
|
|
579
|
+
const raw = typeof integrityConfigFn === "function" ? integrityConfigFn() : null;
|
|
580
|
+
const configuredSources = Array.isArray(raw?.metadataSources) ? raw.metadataSources : null;
|
|
581
|
+
const metadataSources = (configuredSources && configuredSources.length
|
|
582
|
+
? configuredSources
|
|
583
|
+
: DEFAULT_INTEGRITY_CONFIG.metadataSources
|
|
584
|
+
).map((entry) => String(entry).toLowerCase());
|
|
585
|
+
const allowed = new Set(["manifest", "checksums", "sidecar"]);
|
|
586
|
+
const filteredSources = metadataSources.filter((entry) => allowed.has(entry));
|
|
587
|
+
const missingPolicy = raw?.missingPolicy === "abort" ? "abort" : DEFAULT_INTEGRITY_CONFIG.missingPolicy;
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
metadataSources: filteredSources.length ? filteredSources : DEFAULT_INTEGRITY_CONFIG.metadataSources.slice(),
|
|
591
|
+
missingPolicy
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function integrityUnverifiable(reason, { expectedSha256, actualSha256, expectedSource, error } = {}) {
|
|
596
|
+
return {
|
|
597
|
+
status: "unverifiable",
|
|
598
|
+
reason,
|
|
599
|
+
expectedSha256: expectedSha256 ?? null,
|
|
600
|
+
actualSha256: actualSha256 ?? null,
|
|
601
|
+
expectedSource: expectedSource ?? null,
|
|
602
|
+
error: error ?? null
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function integrityMismatch({ expectedSha256, actualSha256, expectedSource }) {
|
|
607
|
+
return {
|
|
608
|
+
status: "mismatch",
|
|
609
|
+
reason: "hash_mismatch",
|
|
610
|
+
expectedSha256: expectedSha256 ?? null,
|
|
611
|
+
actualSha256: actualSha256 ?? null,
|
|
612
|
+
expectedSource: expectedSource ?? null,
|
|
613
|
+
error: null
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function integrityVerified({ expectedSha256, actualSha256, expectedSource }) {
|
|
618
|
+
return {
|
|
619
|
+
status: "verified_ok",
|
|
620
|
+
reason: "hash_match",
|
|
621
|
+
expectedSha256: expectedSha256 ?? null,
|
|
622
|
+
actualSha256: actualSha256 ?? null,
|
|
623
|
+
expectedSource: expectedSource ?? null,
|
|
624
|
+
error: null
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function verifyInstalledDocdexdIntegrity({
|
|
629
|
+
fsModule,
|
|
630
|
+
sha256FileFn,
|
|
631
|
+
binaryPath,
|
|
632
|
+
expectedBinarySha256,
|
|
633
|
+
installedMetadata,
|
|
634
|
+
installedMetadataStatus,
|
|
635
|
+
installedMetadataStatusReason
|
|
636
|
+
}) {
|
|
637
|
+
const existsSync = typeof fsModule?.existsSync === "function" ? fsModule.existsSync.bind(fsModule) : null;
|
|
638
|
+
if (!existsSync) {
|
|
639
|
+
return integrityUnverifiable("fs_unavailable", { expectedSource: null, error: "existsSync_unavailable" });
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!existsSync(binaryPath)) {
|
|
643
|
+
return integrityUnverifiable("missing_file", { expectedSource: null });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const expectedFromRelease = expectedBinarySha256 != null ? normalizeSha256Hex(expectedBinarySha256) : null;
|
|
647
|
+
if (expectedBinarySha256 != null && !expectedFromRelease) {
|
|
648
|
+
return integrityUnverifiable("expected_hash_invalid", { expectedSource: "release" });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const expectedFromMetadata =
|
|
652
|
+
installedMetadataStatus === "valid" ? normalizeSha256Hex(installedMetadata?.binary?.sha256) : null;
|
|
653
|
+
|
|
654
|
+
const expectedSha256 = expectedFromRelease || expectedFromMetadata;
|
|
655
|
+
const expectedSource = expectedFromRelease ? "release" : expectedFromMetadata ? "metadata" : null;
|
|
656
|
+
|
|
657
|
+
if (!expectedSha256) {
|
|
658
|
+
if (installedMetadataStatus && installedMetadataStatus !== "valid") {
|
|
659
|
+
const reason =
|
|
660
|
+
typeof installedMetadataStatusReason === "string" && installedMetadataStatusReason
|
|
661
|
+
? installedMetadataStatusReason
|
|
662
|
+
: "metadata_missing";
|
|
663
|
+
return integrityUnverifiable(reason, { expectedSource });
|
|
664
|
+
}
|
|
665
|
+
return integrityUnverifiable("expected_hash_unavailable", { expectedSource });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
const actualSha256 = normalizeSha256Hex(await sha256FileFn(binaryPath));
|
|
670
|
+
if (!actualSha256) {
|
|
671
|
+
return integrityUnverifiable("hash_unreadable", { expectedSha256, expectedSource });
|
|
672
|
+
}
|
|
673
|
+
if (actualSha256 !== expectedSha256) {
|
|
674
|
+
return integrityMismatch({ expectedSha256, actualSha256, expectedSource });
|
|
675
|
+
}
|
|
676
|
+
return integrityVerified({ expectedSha256, actualSha256, expectedSource });
|
|
677
|
+
} catch (err) {
|
|
678
|
+
return integrityUnverifiable("unreadable", {
|
|
679
|
+
expectedSha256,
|
|
680
|
+
expectedSource,
|
|
681
|
+
error: err?.message || String(err)
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* @param {object} args
|
|
688
|
+
* @param {string} args.expectedVersion
|
|
689
|
+
* @param {{binarySha256: (string|null)}} args.expectedIntegrityMaterial
|
|
690
|
+
* @param {object} args.discoveredInstalledState
|
|
691
|
+
* @param {object} args.integrityResult
|
|
692
|
+
* @returns {{outcome: string, reason: string}}
|
|
693
|
+
*/
|
|
694
|
+
function decideInstallAction({
|
|
695
|
+
expectedVersion,
|
|
696
|
+
expectedIntegrityMaterial,
|
|
697
|
+
discoveredInstalledState,
|
|
698
|
+
integrityResult
|
|
699
|
+
}) {
|
|
700
|
+
if (!discoveredInstalledState?.binaryPresent) return { outcome: "update", reason: "binary_missing" };
|
|
701
|
+
|
|
702
|
+
if (discoveredInstalledState.metadataStatus !== "valid") {
|
|
703
|
+
return {
|
|
704
|
+
outcome: "reinstall_unknown",
|
|
705
|
+
reason: discoveredInstalledState.metadataStatusReason || "metadata_invalid"
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (discoveredInstalledState.platformMismatch) {
|
|
710
|
+
return { outcome: "reinstall_unknown", reason: "platform_mismatch" };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (discoveredInstalledState.installedVersion !== expectedVersion) {
|
|
714
|
+
return { outcome: "update", reason: "version_mismatch" };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const expectedBinarySha256 = normalizeSha256Hex(expectedIntegrityMaterial?.binarySha256);
|
|
718
|
+
if (!expectedBinarySha256) {
|
|
719
|
+
return { outcome: "reinstall_unknown", reason: "expected_integrity_missing" };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (integrityResult?.status === "mismatch") {
|
|
723
|
+
return { outcome: "repair", reason: "binary_integrity_mismatch" };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (integrityResult?.status === "verified_ok") {
|
|
727
|
+
return { outcome: "no-op", reason: "verified" };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return { outcome: "reinstall_unknown", reason: "integrity_unverifiable" };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function discoverInstalledState({ fsModule, pathModule, distDir, platformKey, isWin32 }) {
|
|
734
|
+
const binaryPath = pathModule.join(distDir, isWin32 ? "docdexd.exe" : "docdexd");
|
|
735
|
+
const metadataPath = installMetadataPath(distDir, pathModule);
|
|
736
|
+
|
|
737
|
+
const existsSync = typeof fsModule?.existsSync === "function" ? fsModule.existsSync.bind(fsModule) : null;
|
|
738
|
+
if (!existsSync) {
|
|
739
|
+
return {
|
|
740
|
+
binaryPath,
|
|
741
|
+
metadataPath,
|
|
742
|
+
binaryPresent: false,
|
|
743
|
+
installedVersion: null,
|
|
744
|
+
metadata: null,
|
|
745
|
+
metadataStatus: "unavailable",
|
|
746
|
+
metadataStatusReason: "existsSync_unavailable",
|
|
747
|
+
platformMismatch: false
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (!existsSync(binaryPath)) {
|
|
752
|
+
return {
|
|
753
|
+
binaryPath,
|
|
754
|
+
metadataPath,
|
|
755
|
+
binaryPresent: false,
|
|
756
|
+
installedVersion: null,
|
|
757
|
+
metadata: null,
|
|
758
|
+
metadataStatus: "missing",
|
|
759
|
+
metadataStatusReason: "binary_missing",
|
|
760
|
+
platformMismatch: false
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const metaResult = await readJsonFileIfPossible({ fsModule, filePath: metadataPath });
|
|
765
|
+
const meta = metaResult.value;
|
|
766
|
+
if (!isValidInstallMetadata(meta)) {
|
|
767
|
+
return {
|
|
768
|
+
binaryPath,
|
|
769
|
+
metadataPath,
|
|
770
|
+
binaryPresent: true,
|
|
771
|
+
installedVersion: typeof meta?.version === "string" ? meta.version : null,
|
|
772
|
+
metadata: null,
|
|
773
|
+
metadataStatus:
|
|
774
|
+
metaResult.errorCode === "ENOENT"
|
|
775
|
+
? "missing"
|
|
776
|
+
: metaResult.errorCode
|
|
777
|
+
? "unreadable"
|
|
778
|
+
: "invalid",
|
|
779
|
+
metadataStatusReason:
|
|
780
|
+
metaResult.errorCode === "ENOENT"
|
|
781
|
+
? "metadata_missing"
|
|
782
|
+
: metaResult.errorCode
|
|
783
|
+
? "metadata_unreadable"
|
|
784
|
+
: "metadata_invalid",
|
|
785
|
+
platformMismatch: false
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
binaryPath,
|
|
791
|
+
metadataPath,
|
|
792
|
+
binaryPresent: true,
|
|
793
|
+
installedVersion: meta.version,
|
|
794
|
+
metadata: meta,
|
|
795
|
+
metadataStatus: "valid",
|
|
796
|
+
metadataStatusReason: null,
|
|
797
|
+
platformMismatch: meta.platformKey !== platformKey
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async function verifyInstalledBinaryIntegrity({ sha256FileFn, binaryPath, expectedBinarySha256 }) {
|
|
802
|
+
const expected = normalizeSha256Hex(expectedBinarySha256);
|
|
803
|
+
if (!expected) {
|
|
804
|
+
return integrityUnverifiable("expected_hash_unavailable", {
|
|
805
|
+
expectedSha256: null,
|
|
806
|
+
actualSha256: null,
|
|
807
|
+
expectedSource: null,
|
|
808
|
+
error: "expected_missing"
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
const actual = normalizeSha256Hex(await sha256FileFn(binaryPath));
|
|
814
|
+
if (!actual) {
|
|
815
|
+
return integrityUnverifiable("hash_unreadable", {
|
|
816
|
+
expectedSha256: expected,
|
|
817
|
+
actualSha256: null,
|
|
818
|
+
expectedSource: null,
|
|
819
|
+
error: "actual_invalid"
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (actual !== expected) {
|
|
823
|
+
return integrityMismatch({ expectedSha256: expected, actualSha256: actual, expectedSource: null });
|
|
824
|
+
}
|
|
825
|
+
return integrityVerified({ expectedSha256: expected, actualSha256: actual, expectedSource: null });
|
|
826
|
+
} catch (err) {
|
|
827
|
+
return integrityUnverifiable("unreadable", {
|
|
828
|
+
expectedSha256: expected,
|
|
829
|
+
actualSha256: null,
|
|
830
|
+
expectedSource: null,
|
|
831
|
+
error: err?.message || String(err)
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function determineLocalInstallerOutcome({
|
|
837
|
+
fsModule,
|
|
838
|
+
pathModule,
|
|
839
|
+
distDir,
|
|
840
|
+
platformKey,
|
|
841
|
+
expectedVersion,
|
|
842
|
+
isWin32,
|
|
843
|
+
sha256FileFn = sha256File,
|
|
844
|
+
expectedBinarySha256 = null
|
|
845
|
+
}) {
|
|
846
|
+
const discoveredInstalledState = await discoverInstalledState({
|
|
847
|
+
fsModule,
|
|
848
|
+
pathModule,
|
|
849
|
+
distDir,
|
|
850
|
+
platformKey,
|
|
851
|
+
isWin32
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
const expectedIntegrityMaterial = {
|
|
855
|
+
binarySha256: normalizeSha256Hex(expectedBinarySha256)
|
|
856
|
+
? expectedBinarySha256
|
|
857
|
+
: discoveredInstalledState.metadataStatus === "valid"
|
|
858
|
+
? discoveredInstalledState.metadata.binary.sha256
|
|
859
|
+
: null
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const shouldVerifyIntegrity =
|
|
863
|
+
discoveredInstalledState.binaryPresent &&
|
|
864
|
+
!discoveredInstalledState.platformMismatch &&
|
|
865
|
+
discoveredInstalledState.installedVersion === expectedVersion &&
|
|
866
|
+
(normalizeSha256Hex(expectedBinarySha256) || discoveredInstalledState.metadataStatus === "valid");
|
|
867
|
+
|
|
868
|
+
const integrityResult = shouldVerifyIntegrity
|
|
869
|
+
? await verifyInstalledDocdexdIntegrity({
|
|
870
|
+
fsModule,
|
|
871
|
+
sha256FileFn,
|
|
872
|
+
binaryPath: discoveredInstalledState.binaryPath,
|
|
873
|
+
expectedBinarySha256: expectedBinarySha256,
|
|
874
|
+
installedMetadata: discoveredInstalledState.metadata,
|
|
875
|
+
installedMetadataStatus: discoveredInstalledState.metadataStatus,
|
|
876
|
+
installedMetadataStatusReason: discoveredInstalledState.metadataStatusReason
|
|
877
|
+
})
|
|
878
|
+
: null;
|
|
879
|
+
|
|
880
|
+
const decision = decideInstallAction({
|
|
881
|
+
expectedVersion,
|
|
882
|
+
expectedIntegrityMaterial,
|
|
883
|
+
discoveredInstalledState,
|
|
884
|
+
integrityResult
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
const installedVersion =
|
|
888
|
+
typeof discoveredInstalledState.installedVersion === "string" ? discoveredInstalledState.installedVersion : null;
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
outcome: decision.outcome,
|
|
892
|
+
reason: decision.reason,
|
|
893
|
+
binaryPath: discoveredInstalledState.binaryPath,
|
|
894
|
+
metadataPath: discoveredInstalledState.metadataPath,
|
|
895
|
+
installedVersion,
|
|
896
|
+
integrityResult
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function parseSha256File(text, expectedFilename) {
|
|
901
|
+
const lines = String(text).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
902
|
+
for (const line of lines) {
|
|
903
|
+
// Typical format: "<hex> <filename>"
|
|
904
|
+
const match = line.match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
|
|
905
|
+
if (!match) continue;
|
|
906
|
+
const hash = match[1].toLowerCase();
|
|
907
|
+
const filename = match[2].trim();
|
|
908
|
+
if (!expectedFilename || filename === expectedFilename) return hash;
|
|
909
|
+
}
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function checksumCandidateNames() {
|
|
914
|
+
const envNames = process.env.DOCDEX_CHECKSUMS_NAMES || process.env.DOCDEX_CHECKSUMS_NAME;
|
|
915
|
+
if (envNames) {
|
|
916
|
+
return envNames
|
|
917
|
+
.split(",")
|
|
918
|
+
.map((s) => s.trim())
|
|
919
|
+
.filter(Boolean);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Documented fallback (ops-01-us-08): SHA256SUMS from the same GitHub Release.
|
|
923
|
+
return ["SHA256SUMS", "SHA256SUMS.txt"];
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function manifestCandidateNames() {
|
|
927
|
+
const envNames = process.env.DOCDEX_MANIFEST_NAMES || process.env.DOCDEX_MANIFEST_NAME;
|
|
928
|
+
if (envNames) {
|
|
929
|
+
return envNames
|
|
930
|
+
.split(",")
|
|
931
|
+
.map((s) => s.trim())
|
|
932
|
+
.filter(Boolean);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Assumption (documented by code): release attaches one of these filenames.
|
|
936
|
+
return [
|
|
937
|
+
"docdex-release-manifest.json",
|
|
938
|
+
// Legacy/compat candidates:
|
|
939
|
+
"docdexd-manifest.json",
|
|
940
|
+
"docdex-manifest.json",
|
|
941
|
+
"manifest.json"
|
|
942
|
+
];
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function tryResolveSha256ViaChecksumFiles({
|
|
946
|
+
repoSlug,
|
|
947
|
+
version,
|
|
948
|
+
archive,
|
|
949
|
+
downloadTextFn = downloadText,
|
|
950
|
+
getDownloadBaseFn = getDownloadBase,
|
|
951
|
+
checksumCandidateNamesFn = checksumCandidateNames
|
|
952
|
+
}) {
|
|
953
|
+
const base = getDownloadBaseFn(repoSlug);
|
|
954
|
+
const candidates = checksumCandidateNamesFn();
|
|
955
|
+
const errors = [];
|
|
956
|
+
const events = [];
|
|
957
|
+
|
|
958
|
+
for (const name of candidates) {
|
|
959
|
+
const url = `${base}/v${version}/${name}`;
|
|
960
|
+
try {
|
|
961
|
+
const text = await downloadTextFn(url);
|
|
962
|
+
const parsed = parseSha256File(text, archive);
|
|
963
|
+
if (parsed) {
|
|
964
|
+
return { checksumName: name, checksumUrl: url, sha256: parsed, errors, events, attempted: true };
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const message = `Checksum file (${name}) is missing an entry for ${archive}`;
|
|
968
|
+
errors.push(`[DOCDEX_CHECKSUM_ENTRY_MISSING] ${message}`);
|
|
969
|
+
events.push({ code: "DOCDEX_CHECKSUM_ENTRY_MISSING", message, details: { checksumName: name, url, archive } });
|
|
970
|
+
continue;
|
|
971
|
+
} catch (e) {
|
|
972
|
+
// 404 => missing candidate; try next.
|
|
973
|
+
if (e && typeof e.statusCode === "number" && e.statusCode === 404) {
|
|
974
|
+
events.push({
|
|
975
|
+
code: "DOCDEX_CHECKSUM_NOT_FOUND",
|
|
976
|
+
message: `Checksum candidate not found (${name})`,
|
|
977
|
+
details: { checksumName: name, url, archive, statusCode: 404 }
|
|
978
|
+
});
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (e && e.code === "DOCDEX_DOWNLOAD_TOO_LARGE") {
|
|
983
|
+
const message = `Checksum file too large (${name}): exceeded ${e.maxBytes} bytes`;
|
|
984
|
+
errors.push(`[DOCDEX_CHECKSUM_TOO_LARGE] ${message}`);
|
|
985
|
+
events.push({
|
|
986
|
+
code: "DOCDEX_CHECKSUM_TOO_LARGE",
|
|
987
|
+
message,
|
|
988
|
+
details: { checksumName: name, url, archive, maxBytes: e.maxBytes, actualBytes: e.actualBytes }
|
|
989
|
+
});
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const message = `Failed to fetch checksum file (${name}): ${e.message}`;
|
|
994
|
+
errors.push(`[DOCDEX_CHECKSUM_FETCH_FAILED] ${message}`);
|
|
995
|
+
events.push({
|
|
996
|
+
code: "DOCDEX_CHECKSUM_FETCH_FAILED",
|
|
997
|
+
message,
|
|
998
|
+
details: {
|
|
999
|
+
checksumName: name,
|
|
1000
|
+
url,
|
|
1001
|
+
archive,
|
|
1002
|
+
statusCode: typeof e?.statusCode === "number" ? e.statusCode : null
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
return { checksumName: null, checksumUrl: null, sha256: null, errors, events, attempted: true, candidates };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function tryResolveAssetViaManifest({
|
|
1013
|
+
repoSlug,
|
|
1014
|
+
version,
|
|
1015
|
+
targetTriple,
|
|
1016
|
+
downloadTextFn = downloadText,
|
|
1017
|
+
getDownloadBaseFn = getDownloadBase,
|
|
1018
|
+
manifestCandidateNamesFn = manifestCandidateNames
|
|
1019
|
+
}) {
|
|
1020
|
+
const base = getDownloadBaseFn(repoSlug);
|
|
1021
|
+
const errors = [];
|
|
1022
|
+
const events = [];
|
|
1023
|
+
const candidates = manifestCandidateNamesFn();
|
|
1024
|
+
|
|
1025
|
+
for (const name of candidates) {
|
|
1026
|
+
const url = `${base}/v${version}/${name}`;
|
|
1027
|
+
try {
|
|
1028
|
+
const text = await downloadTextFn(url);
|
|
1029
|
+
let manifest;
|
|
1030
|
+
try {
|
|
1031
|
+
manifest = JSON.parse(text);
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
const message = `Malformed manifest (${name}): ${INVALID_JSON_ERROR}`;
|
|
1034
|
+
errors.push(`[DOCDEX_MANIFEST_JSON_INVALID] ${message}`);
|
|
1035
|
+
events.push({
|
|
1036
|
+
code: "DOCDEX_MANIFEST_JSON_INVALID",
|
|
1037
|
+
message,
|
|
1038
|
+
details: { manifestName: name, url, targetTriple }
|
|
1039
|
+
});
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// If a manifest exists but doesn't support the current triple, fail deterministically.
|
|
1044
|
+
try {
|
|
1045
|
+
return {
|
|
1046
|
+
manifestName: name,
|
|
1047
|
+
resolved: resolveCanonicalAssetForTargetTriple(manifest, targetTriple),
|
|
1048
|
+
errors,
|
|
1049
|
+
events,
|
|
1050
|
+
attempted: true
|
|
1051
|
+
};
|
|
1052
|
+
} catch (e) {
|
|
1053
|
+
if (e instanceof ManifestResolutionError) {
|
|
1054
|
+
// Fail closed when the manifest is present but resolution is unsupported or ambiguous.
|
|
1055
|
+
if (e.code === "DOCDEX_ASSET_NO_MATCH" || e.code === "DOCDEX_ASSET_MULTI_MATCH") {
|
|
1056
|
+
e.message = `Manifest ${name}: ${e.message}`;
|
|
1057
|
+
e.details = {
|
|
1058
|
+
...withBaseDetails(e.details),
|
|
1059
|
+
manifestName: name,
|
|
1060
|
+
manifestUrl: url,
|
|
1061
|
+
fallbackAttempted: false,
|
|
1062
|
+
fallbackReason: "manifest_present_but_unusable"
|
|
1063
|
+
};
|
|
1064
|
+
throw e;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Missing keys / invalid shape / missing sha256: treat as malformed and deterministically fall back.
|
|
1068
|
+
const message = `Manifest unusable (${name}): ${e.code} ${e.message}`;
|
|
1069
|
+
errors.push(`[DOCDEX_MANIFEST_UNUSABLE] ${message}`);
|
|
1070
|
+
events.push({
|
|
1071
|
+
code: "DOCDEX_MANIFEST_UNUSABLE",
|
|
1072
|
+
message,
|
|
1073
|
+
details: {
|
|
1074
|
+
manifestName: name,
|
|
1075
|
+
url,
|
|
1076
|
+
targetTriple,
|
|
1077
|
+
manifestErrorCode: e.code,
|
|
1078
|
+
manifestErrorMessage: e.message
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
throw e;
|
|
1084
|
+
}
|
|
1085
|
+
} catch (e) {
|
|
1086
|
+
if (e instanceof ManifestResolutionError) throw e;
|
|
1087
|
+
// 404 => "missing manifest" candidate; try next. Anything else is recorded and we still try next.
|
|
1088
|
+
if (e && typeof e.statusCode === "number" && e.statusCode === 404) {
|
|
1089
|
+
events.push({
|
|
1090
|
+
code: "DOCDEX_MANIFEST_NOT_FOUND",
|
|
1091
|
+
message: `Manifest candidate not found (${name})`,
|
|
1092
|
+
details: { manifestName: name, url, targetTriple, statusCode: 404 }
|
|
1093
|
+
});
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (e && e.code === "DOCDEX_DOWNLOAD_TOO_LARGE") {
|
|
1098
|
+
const message = `Manifest too large (${name}): exceeded ${e.maxBytes} bytes`;
|
|
1099
|
+
errors.push(`[DOCDEX_MANIFEST_TOO_LARGE] ${message}`);
|
|
1100
|
+
events.push({
|
|
1101
|
+
code: "DOCDEX_MANIFEST_TOO_LARGE",
|
|
1102
|
+
message,
|
|
1103
|
+
details: { manifestName: name, url, targetTriple, maxBytes: e.maxBytes, actualBytes: e.actualBytes }
|
|
1104
|
+
});
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const message = `Failed to fetch manifest (${name}): ${e.message}`;
|
|
1109
|
+
errors.push(`[DOCDEX_MANIFEST_FETCH_FAILED] ${message}`);
|
|
1110
|
+
events.push({
|
|
1111
|
+
code: "DOCDEX_MANIFEST_FETCH_FAILED",
|
|
1112
|
+
message,
|
|
1113
|
+
details: {
|
|
1114
|
+
manifestName: name,
|
|
1115
|
+
url,
|
|
1116
|
+
targetTriple,
|
|
1117
|
+
statusCode: typeof e?.statusCode === "number" ? e.statusCode : null
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (candidates.length) {
|
|
1125
|
+
events.push({
|
|
1126
|
+
code: "DOCDEX_FALLBACK_USED",
|
|
1127
|
+
message: "No usable manifest candidate; falling back to deterministic asset naming",
|
|
1128
|
+
details: { targetTriple, manifestCandidates: candidates.slice() }
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return { manifestName: null, resolved: null, errors, events, attempted: true };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function resolveInstallerDownloadPlan({
|
|
1136
|
+
repoSlug,
|
|
1137
|
+
version,
|
|
1138
|
+
platformKey,
|
|
1139
|
+
targetTriple,
|
|
1140
|
+
logger = console,
|
|
1141
|
+
downloadTextFn = downloadText,
|
|
1142
|
+
artifactNameFn = artifactName,
|
|
1143
|
+
getDownloadBaseFn = getDownloadBase,
|
|
1144
|
+
manifestCandidateNamesFn = manifestCandidateNames,
|
|
1145
|
+
checksumCandidateNamesFn = checksumCandidateNames,
|
|
1146
|
+
integrityConfigFn
|
|
1147
|
+
}) {
|
|
1148
|
+
let archive = null;
|
|
1149
|
+
let expectedSha256 = null;
|
|
1150
|
+
let source = "fallback";
|
|
1151
|
+
let integrity = null;
|
|
1152
|
+
|
|
1153
|
+
const integrityConfig = resolveIntegrityConfig(integrityConfigFn);
|
|
1154
|
+
const attemptedSources = [];
|
|
1155
|
+
|
|
1156
|
+
let manifestAttempt = { resolved: null, errors: [], manifestName: null };
|
|
1157
|
+
|
|
1158
|
+
if (integrityConfig.metadataSources.includes("manifest")) {
|
|
1159
|
+
attemptedSources.push("manifest");
|
|
1160
|
+
try {
|
|
1161
|
+
manifestAttempt = await tryResolveAssetViaManifest({
|
|
1162
|
+
repoSlug,
|
|
1163
|
+
version,
|
|
1164
|
+
targetTriple,
|
|
1165
|
+
downloadTextFn,
|
|
1166
|
+
getDownloadBaseFn,
|
|
1167
|
+
manifestCandidateNamesFn
|
|
1168
|
+
});
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
if (err instanceof ManifestResolutionError) {
|
|
1171
|
+
const expectedAsset = artifactNameFn(platformKey);
|
|
1172
|
+
err.details = {
|
|
1173
|
+
...withBaseDetails(err.details),
|
|
1174
|
+
platformKey,
|
|
1175
|
+
expectedAsset,
|
|
1176
|
+
expectedAssetPattern: assetPatternForPlatformKey(platformKey, { exampleAssetName: expectedAsset })
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
throw err;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (manifestAttempt.resolved) {
|
|
1183
|
+
archive = manifestAttempt.resolved.asset.name;
|
|
1184
|
+
expectedSha256 = manifestAttempt.resolved.integrity.sha256;
|
|
1185
|
+
source = `manifest:${manifestAttempt.manifestName}`;
|
|
1186
|
+
integrity = { metadataSource: "manifest", metadataName: manifestAttempt.manifestName };
|
|
1187
|
+
} else if (manifestAttempt.errors && manifestAttempt.errors.length) {
|
|
1188
|
+
logger.warn(`[docdex] Manifest unavailable; falling back. Details: ${manifestAttempt.errors.join(" | ")}`);
|
|
1189
|
+
} else {
|
|
1190
|
+
logger.log("[docdex] No manifest found; falling back to deterministic asset naming.");
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (!manifestAttempt.resolved && integrityConfig.missingPolicy === "abort") {
|
|
1194
|
+
const manifestCandidates = manifestCandidateNamesFn();
|
|
1195
|
+
const checksumCandidates = checksumCandidateNamesFn();
|
|
1196
|
+
throw new ChecksumResolutionError(
|
|
1197
|
+
`Missing SHA-256 integrity metadata for ${artifactNameFn(platformKey)} (manifest fetch aborted)`,
|
|
1198
|
+
{
|
|
1199
|
+
platformKey,
|
|
1200
|
+
targetTriple,
|
|
1201
|
+
version,
|
|
1202
|
+
repoSlug,
|
|
1203
|
+
assetName: artifactNameFn(platformKey),
|
|
1204
|
+
source: "fallback",
|
|
1205
|
+
manifestName: manifestAttempt?.manifestName ?? null,
|
|
1206
|
+
manifestVersion: manifestAttempt?.resolved?.manifestVersion ?? null,
|
|
1207
|
+
fallbackAttempted: true,
|
|
1208
|
+
fallbackReason: manifestAttempt?.errors?.length ? "manifest_unavailable" : "manifest_not_found",
|
|
1209
|
+
checksumCandidates,
|
|
1210
|
+
checksumErrors: null,
|
|
1211
|
+
checksumEvents: null,
|
|
1212
|
+
integrityMissingPolicy: integrityConfig.missingPolicy,
|
|
1213
|
+
integrityAttemptedSources: attemptedSources.slice()
|
|
1214
|
+
}
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (!archive) {
|
|
1220
|
+
archive = artifactNameFn(platformKey);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
let checksumAttempt = null;
|
|
1224
|
+
if (!expectedSha256 && integrityConfig.metadataSources.includes("checksums")) {
|
|
1225
|
+
attemptedSources.push("checksums");
|
|
1226
|
+
checksumAttempt = await tryResolveSha256ViaChecksumFiles({
|
|
1227
|
+
repoSlug,
|
|
1228
|
+
version,
|
|
1229
|
+
archive,
|
|
1230
|
+
downloadTextFn,
|
|
1231
|
+
getDownloadBaseFn,
|
|
1232
|
+
checksumCandidateNamesFn
|
|
1233
|
+
});
|
|
1234
|
+
if (checksumAttempt.sha256) {
|
|
1235
|
+
expectedSha256 = checksumAttempt.sha256;
|
|
1236
|
+
integrity = {
|
|
1237
|
+
metadataSource: "checksums",
|
|
1238
|
+
metadataName: checksumAttempt.checksumName
|
|
1239
|
+
};
|
|
1240
|
+
} else if (integrityConfig.missingPolicy === "abort") {
|
|
1241
|
+
const checksumCandidates = checksumCandidateNamesFn();
|
|
1242
|
+
throw new ChecksumResolutionError(
|
|
1243
|
+
`Missing SHA-256 integrity metadata for ${archive} (checksums fetch aborted)`,
|
|
1244
|
+
{
|
|
1245
|
+
platformKey,
|
|
1246
|
+
targetTriple,
|
|
1247
|
+
version,
|
|
1248
|
+
repoSlug,
|
|
1249
|
+
assetName: archive,
|
|
1250
|
+
source: "fallback",
|
|
1251
|
+
manifestName: manifestAttempt?.manifestName ?? null,
|
|
1252
|
+
manifestVersion: manifestAttempt?.resolved?.manifestVersion ?? null,
|
|
1253
|
+
fallbackAttempted: true,
|
|
1254
|
+
fallbackReason: manifestAttempt?.errors?.length ? "manifest_unavailable" : "manifest_not_found",
|
|
1255
|
+
checksumCandidates,
|
|
1256
|
+
checksumErrors: checksumAttempt?.errors ?? null,
|
|
1257
|
+
checksumEvents: checksumAttempt?.events ?? null,
|
|
1258
|
+
integrityMissingPolicy: integrityConfig.missingPolicy,
|
|
1259
|
+
integrityAttemptedSources: attemptedSources.slice()
|
|
1260
|
+
}
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (!expectedSha256 && integrityConfig.metadataSources.includes("sidecar")) {
|
|
1266
|
+
attemptedSources.push("sidecar");
|
|
1267
|
+
const shaUrl = `${getDownloadBaseFn(repoSlug)}/v${version}/${archive}.sha256`;
|
|
1268
|
+
try {
|
|
1269
|
+
const shaText = await downloadTextFn(shaUrl);
|
|
1270
|
+
expectedSha256 = parseSha256File(shaText, archive);
|
|
1271
|
+
if (expectedSha256) {
|
|
1272
|
+
integrity = { metadataSource: "sidecar", metadataName: `${archive}.sha256` };
|
|
1273
|
+
}
|
|
1274
|
+
} catch {
|
|
1275
|
+
expectedSha256 = null;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (!expectedSha256) {
|
|
1280
|
+
const manifestCandidates = manifestCandidateNamesFn();
|
|
1281
|
+
const checksumCandidates = checksumCandidateNamesFn();
|
|
1282
|
+
throw new ChecksumResolutionError(
|
|
1283
|
+
`Missing SHA-256 integrity metadata for ${archive} (tried manifest ${manifestCandidates.join(
|
|
1284
|
+
", "
|
|
1285
|
+
)} and checksums ${checksumCandidates.join(", ")})`,
|
|
1286
|
+
{
|
|
1287
|
+
platformKey,
|
|
1288
|
+
targetTriple,
|
|
1289
|
+
version,
|
|
1290
|
+
repoSlug,
|
|
1291
|
+
assetName: archive,
|
|
1292
|
+
source: "fallback",
|
|
1293
|
+
manifestName: manifestAttempt?.manifestName ?? null,
|
|
1294
|
+
manifestVersion: manifestAttempt?.resolved?.manifestVersion ?? null,
|
|
1295
|
+
fallbackAttempted: true,
|
|
1296
|
+
fallbackReason: manifestAttempt?.errors?.length ? "manifest_unavailable" : "manifest_not_found",
|
|
1297
|
+
checksumCandidates,
|
|
1298
|
+
checksumErrors: checksumAttempt?.errors ?? null,
|
|
1299
|
+
checksumEvents: checksumAttempt?.events ?? null,
|
|
1300
|
+
integrityMissingPolicy: integrityConfig.missingPolicy,
|
|
1301
|
+
integrityAttemptedSources: attemptedSources.slice()
|
|
1302
|
+
}
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
return {
|
|
1307
|
+
archive,
|
|
1308
|
+
expectedSha256,
|
|
1309
|
+
source,
|
|
1310
|
+
integrity,
|
|
1311
|
+
manifestAttempt: { ...manifestAttempt, fallbackAttempted: !manifestAttempt.resolved }
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function verifyDownloadedFileIntegrity({
|
|
1316
|
+
filePath,
|
|
1317
|
+
expectedSha256,
|
|
1318
|
+
archiveName,
|
|
1319
|
+
sha256FileFn = sha256File,
|
|
1320
|
+
details
|
|
1321
|
+
}) {
|
|
1322
|
+
if (!expectedSha256) return null;
|
|
1323
|
+
const actual = await sha256FileFn(filePath);
|
|
1324
|
+
if (actual.toLowerCase() !== expectedSha256.toLowerCase()) {
|
|
1325
|
+
throw new IntegrityMismatchError(archiveName, expectedSha256, actual, details);
|
|
1326
|
+
}
|
|
1327
|
+
return actual;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
async function runInstaller(options) {
|
|
1331
|
+
const opts = options || {};
|
|
1332
|
+
const logger = opts.logger || console;
|
|
1333
|
+
|
|
1334
|
+
const detectPlatformKeyFn = opts.detectPlatformKeyFn || detectPlatformKey;
|
|
1335
|
+
const targetTripleForPlatformKeyFn = opts.targetTripleForPlatformKeyFn || targetTripleForPlatformKey;
|
|
1336
|
+
const getVersionFn = opts.getVersionFn || getVersion;
|
|
1337
|
+
const parseRepoSlugFn = opts.parseRepoSlugFn || parseRepoSlug;
|
|
1338
|
+
const resolveInstallerDownloadPlanFn = opts.resolveInstallerDownloadPlanFn || resolveInstallerDownloadPlan;
|
|
1339
|
+
const getDownloadBaseFn = opts.getDownloadBaseFn || getDownloadBase;
|
|
1340
|
+
const downloadFn = opts.downloadFn || download;
|
|
1341
|
+
const verifyDownloadedFileIntegrityFn = opts.verifyDownloadedFileIntegrityFn || verifyDownloadedFileIntegrity;
|
|
1342
|
+
const extractTarballFn = opts.extractTarballFn || extractTarball;
|
|
1343
|
+
const fsModule = opts.fsModule || fs;
|
|
1344
|
+
const pathModule = opts.pathModule || path;
|
|
1345
|
+
const osModule = opts.osModule || os;
|
|
1346
|
+
const artifactNameFn = opts.artifactNameFn || artifactName;
|
|
1347
|
+
const assetPatternForPlatformKeyFn = opts.assetPatternForPlatformKeyFn || assetPatternForPlatformKey;
|
|
1348
|
+
const sha256FileFn = opts.sha256FileFn || sha256File;
|
|
1349
|
+
const writeJsonFileAtomicFn = opts.writeJsonFileAtomicFn || writeJsonFileAtomic;
|
|
1350
|
+
const restartFn = opts.restartFn;
|
|
1351
|
+
|
|
1352
|
+
const detectedPlatform = opts.platform || process.platform;
|
|
1353
|
+
const detectedArch = opts.arch || process.arch;
|
|
1354
|
+
|
|
1355
|
+
const resolvePlatformPolicyFn =
|
|
1356
|
+
opts.resolvePlatformPolicyFn ||
|
|
1357
|
+
(opts.detectPlatformKeyFn || opts.targetTripleForPlatformKeyFn
|
|
1358
|
+
? () => {
|
|
1359
|
+
const platformKey = detectPlatformKeyFn();
|
|
1360
|
+
const targetTriple = targetTripleForPlatformKeyFn(platformKey);
|
|
1361
|
+
const expectedAssetName = artifactNameFn(platformKey);
|
|
1362
|
+
const expectedAssetPattern = assetPatternForPlatformKeyFn(platformKey, {
|
|
1363
|
+
exampleAssetName: expectedAssetName
|
|
1364
|
+
});
|
|
1365
|
+
return {
|
|
1366
|
+
detected: { platform: detectedPlatform, arch: detectedArch },
|
|
1367
|
+
platformKey,
|
|
1368
|
+
targetTriple,
|
|
1369
|
+
expectedAssetName,
|
|
1370
|
+
expectedAssetPattern
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
: resolvePlatformPolicy);
|
|
1374
|
+
|
|
1375
|
+
const platformPolicy = resolvePlatformPolicyFn({
|
|
1376
|
+
platform: detectedPlatform,
|
|
1377
|
+
arch: detectedArch,
|
|
1378
|
+
env: opts.env,
|
|
1379
|
+
report: opts.report,
|
|
1380
|
+
execPath: opts.execPath
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
const platformKey = platformPolicy.platformKey;
|
|
1384
|
+
const targetTriple = platformPolicy.targetTriple;
|
|
1385
|
+
const version = getVersionFn();
|
|
1386
|
+
const distBaseDir = opts.distBaseDir || pathModule.join(__dirname, "..", "dist");
|
|
1387
|
+
const distDir = pathModule.join(distBaseDir, platformKey);
|
|
1388
|
+
const isWin32 = detectedPlatform === "win32";
|
|
1389
|
+
|
|
1390
|
+
const existsSync = typeof fsModule?.existsSync === "function" ? fsModule.existsSync.bind(fsModule) : null;
|
|
1391
|
+
|
|
1392
|
+
const preflight = await recoverInterruptedInstall({ fsModule, pathModule, distDir, isWin32, logger });
|
|
1393
|
+
|
|
1394
|
+
const local = await determineLocalInstallerOutcome({
|
|
1395
|
+
fsModule,
|
|
1396
|
+
pathModule,
|
|
1397
|
+
distDir,
|
|
1398
|
+
platformKey,
|
|
1399
|
+
expectedVersion: version,
|
|
1400
|
+
isWin32,
|
|
1401
|
+
sha256FileFn
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
const priorRunnable = existsSync ? existsSync(local.binaryPath) : false;
|
|
1405
|
+
|
|
1406
|
+
if (local.outcome === "no-op") {
|
|
1407
|
+
logger.log("[docdex] Install outcome: no-op");
|
|
1408
|
+
await cleanupInstallArtifacts({
|
|
1409
|
+
fsModule,
|
|
1410
|
+
pathModule,
|
|
1411
|
+
distBaseDir,
|
|
1412
|
+
distDir,
|
|
1413
|
+
platformKey,
|
|
1414
|
+
stagingDir: null,
|
|
1415
|
+
backupDir: null
|
|
1416
|
+
});
|
|
1417
|
+
emitInstallerEvent(logger, {
|
|
1418
|
+
code: "DOCDEX_INSTALL_OUTCOME",
|
|
1419
|
+
details: {
|
|
1420
|
+
outcome: local.outcome,
|
|
1421
|
+
outcomeCode: "noop",
|
|
1422
|
+
reason: local.reason,
|
|
1423
|
+
downloadAttempted: false
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
return {
|
|
1427
|
+
binaryPath: local.binaryPath,
|
|
1428
|
+
outcome: local.outcome,
|
|
1429
|
+
outcomeCode: "noop",
|
|
1430
|
+
integrityResult: local.integrityResult
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const repoSlug = parseRepoSlugFn();
|
|
1435
|
+
|
|
1436
|
+
const { archive, expectedSha256, source, manifestAttempt } = await resolveInstallerDownloadPlanFn({
|
|
1437
|
+
repoSlug,
|
|
1438
|
+
version,
|
|
1439
|
+
platformKey,
|
|
1440
|
+
targetTriple,
|
|
1441
|
+
logger,
|
|
1442
|
+
integrityConfigFn: opts.integrityConfigFn
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
const downloadUrl = `${getDownloadBaseFn(repoSlug)}/v${version}/${archive}`;
|
|
1446
|
+
const nonce = buildInstallNonce();
|
|
1447
|
+
const stagingDir = stageDirName({ distDir, nonce });
|
|
1448
|
+
const backupDir = backupDirName({ distDir, nonce });
|
|
1449
|
+
const failedDir = failedDirName({ distDir, nonce });
|
|
1450
|
+
const tmpDir = opts.tmpDir || null;
|
|
1451
|
+
let tmpFile = null;
|
|
1452
|
+
if (tmpDir) {
|
|
1453
|
+
tmpFile = pathModule.join(tmpDir, `${archive}.${process.pid}.tgz`);
|
|
1454
|
+
} else {
|
|
1455
|
+
const downloadRoot = stagingDownloadDir({ distBaseDir, platformKey, pathModule });
|
|
1456
|
+
await fsModule.promises.mkdir(downloadRoot, { recursive: true });
|
|
1457
|
+
tmpFile = pathModule.join(downloadRoot, `.docdex-download-staging-${nonce}.tgz`);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const installAttempt = {
|
|
1461
|
+
stagingDir,
|
|
1462
|
+
backupDir,
|
|
1463
|
+
failedDir,
|
|
1464
|
+
tmpDownloadPath: tmpFile,
|
|
1465
|
+
distDir,
|
|
1466
|
+
archive,
|
|
1467
|
+
version
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
const installSafety = {
|
|
1471
|
+
status: "in_progress",
|
|
1472
|
+
preflightRecovery:
|
|
1473
|
+
preflight?.action === "recovered"
|
|
1474
|
+
? "recovered backup"
|
|
1475
|
+
: preflight?.action === "recovery_failed"
|
|
1476
|
+
? "recovery failed"
|
|
1477
|
+
: "not needed",
|
|
1478
|
+
priorRunnable: { before: priorRunnable, after: null },
|
|
1479
|
+
rollback: "not needed",
|
|
1480
|
+
cleanup: "none"
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
logger.log(`[docdex] Fetching ${archive} for ${platformKey} (${targetTriple}) via ${source}...`);
|
|
1484
|
+
let downloadAttempted = false;
|
|
1485
|
+
let backupMoved = false;
|
|
1486
|
+
let promoted = false;
|
|
1487
|
+
let extractAttempted = false;
|
|
1488
|
+
try {
|
|
1489
|
+
try {
|
|
1490
|
+
downloadAttempted = true;
|
|
1491
|
+
await downloadFn(downloadUrl, tmpFile);
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
if (err && typeof err.statusCode === "number" && err.statusCode === 404) {
|
|
1494
|
+
const fallbackReason = manifestAttempt?.errors?.length ? "manifest_unavailable" : "manifest_not_found";
|
|
1495
|
+
throw new MissingArtifactError({
|
|
1496
|
+
detected: { os: detectedPlatform, arch: detectedArch },
|
|
1497
|
+
platformKey,
|
|
1498
|
+
targetTriple,
|
|
1499
|
+
assetName: archive,
|
|
1500
|
+
source,
|
|
1501
|
+
manifestName: manifestAttempt?.manifestName ?? null,
|
|
1502
|
+
manifestVersion: manifestAttempt?.resolved?.manifestVersion ?? null,
|
|
1503
|
+
fallbackAttempted: source === "fallback",
|
|
1504
|
+
fallbackReason,
|
|
1505
|
+
version,
|
|
1506
|
+
repoSlug,
|
|
1507
|
+
downloadUrl,
|
|
1508
|
+
expectedAsset: archive,
|
|
1509
|
+
expectedAssetPattern: assetPatternForPlatformKeyFn(platformKey, { exampleAssetName: archive }),
|
|
1510
|
+
note: "This usually means the GitHub release assets are missing or the npm version is out of sync with the release."
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
throw new DownloadError(
|
|
1514
|
+
`Download failed for ${archive}`,
|
|
1515
|
+
{
|
|
1516
|
+
platformKey,
|
|
1517
|
+
targetTriple,
|
|
1518
|
+
version,
|
|
1519
|
+
repoSlug,
|
|
1520
|
+
assetName: archive,
|
|
1521
|
+
downloadUrl,
|
|
1522
|
+
source,
|
|
1523
|
+
manifestName: manifestAttempt?.manifestName ?? null,
|
|
1524
|
+
manifestVersion: manifestAttempt?.resolved?.manifestVersion ?? null,
|
|
1525
|
+
fallbackAttempted: source === "fallback",
|
|
1526
|
+
statusCode: typeof err?.statusCode === "number" ? err.statusCode : null
|
|
1527
|
+
},
|
|
1528
|
+
err
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
try {
|
|
1533
|
+
await verifyDownloadedFileIntegrityFn({
|
|
1534
|
+
filePath: tmpFile,
|
|
1535
|
+
expectedSha256,
|
|
1536
|
+
archiveName: archive,
|
|
1537
|
+
details: {
|
|
1538
|
+
platformKey,
|
|
1539
|
+
targetTriple,
|
|
1540
|
+
version,
|
|
1541
|
+
repoSlug,
|
|
1542
|
+
downloadUrl,
|
|
1543
|
+
source,
|
|
1544
|
+
manifestName: manifestAttempt?.manifestName ?? null,
|
|
1545
|
+
manifestVersion: manifestAttempt?.resolved?.manifestVersion ?? null,
|
|
1546
|
+
fallbackAttempted: source === "fallback"
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
if (err instanceof IntegrityMismatchError) {
|
|
1551
|
+
emitInstallerEvent(logger, {
|
|
1552
|
+
code: "DOCDEX_INSTALL_INTEGRITY_ARCHIVE",
|
|
1553
|
+
details: {
|
|
1554
|
+
status: "mismatch",
|
|
1555
|
+
expectedSha256: err.details?.expectedSha256 ?? null,
|
|
1556
|
+
actualSha256: err.details?.actualSha256 ?? null
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
throw err;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
extractAttempted = true;
|
|
1564
|
+
await extractTarballFn(tmpFile, stagingDir);
|
|
1565
|
+
|
|
1566
|
+
const stagedBinaryPath = pathModule.join(stagingDir, isWin32 ? "docdexd.exe" : "docdexd");
|
|
1567
|
+
if (existsSync && !existsSync(stagedBinaryPath)) {
|
|
1568
|
+
throw new ArchiveInvalidError(`Downloaded archive missing binary at ${stagedBinaryPath}`, {
|
|
1569
|
+
platformKey,
|
|
1570
|
+
targetTriple,
|
|
1571
|
+
version,
|
|
1572
|
+
repoSlug,
|
|
1573
|
+
assetName: archive,
|
|
1574
|
+
downloadUrl,
|
|
1575
|
+
source,
|
|
1576
|
+
manifestName: manifestAttempt?.manifestName ?? null,
|
|
1577
|
+
manifestVersion: manifestAttempt?.resolved?.manifestVersion ?? null,
|
|
1578
|
+
fallbackAttempted: source === "fallback",
|
|
1579
|
+
binaryPath: stagedBinaryPath
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
await fsModule.promises.chmod(stagedBinaryPath, 0o755);
|
|
1584
|
+
|
|
1585
|
+
if (existsSync && existsSync(distDir)) {
|
|
1586
|
+
await fsModule.promises.rm(backupDir, { recursive: true, force: true }).catch(() => {});
|
|
1587
|
+
await fsModule.promises.rename(distDir, backupDir);
|
|
1588
|
+
backupMoved = true;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
await fsModule.promises.rename(stagingDir, distDir);
|
|
1592
|
+
promoted = true;
|
|
1593
|
+
|
|
1594
|
+
if (typeof restartFn === "function") {
|
|
1595
|
+
await restartFn();
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const binaryPath = pathModule.join(distDir, isWin32 ? "docdexd.exe" : "docdexd");
|
|
1599
|
+
const binarySha256 = await sha256FileFn(binaryPath);
|
|
1600
|
+
const metadata = {
|
|
1601
|
+
schemaVersion: INSTALL_METADATA_SCHEMA_VERSION,
|
|
1602
|
+
installedAt: nowIso(),
|
|
1603
|
+
version,
|
|
1604
|
+
repoSlug,
|
|
1605
|
+
platformKey,
|
|
1606
|
+
targetTriple,
|
|
1607
|
+
binary: {
|
|
1608
|
+
filename: isWin32 ? "docdexd.exe" : "docdexd",
|
|
1609
|
+
sha256: binarySha256
|
|
1610
|
+
},
|
|
1611
|
+
archive: {
|
|
1612
|
+
name: archive,
|
|
1613
|
+
sha256: expectedSha256 || null,
|
|
1614
|
+
source,
|
|
1615
|
+
downloadUrl
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
await writeJsonFileAtomicFn({
|
|
1619
|
+
fsModule,
|
|
1620
|
+
pathModule,
|
|
1621
|
+
filePath: installMetadataPath(distDir, pathModule),
|
|
1622
|
+
value: metadata
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
if (backupMoved) {
|
|
1626
|
+
await removeDirSafe(fsModule, backupDir);
|
|
1627
|
+
}
|
|
1628
|
+
await cleanupInstallArtifacts({
|
|
1629
|
+
fsModule,
|
|
1630
|
+
pathModule,
|
|
1631
|
+
distBaseDir,
|
|
1632
|
+
distDir,
|
|
1633
|
+
platformKey,
|
|
1634
|
+
stagingDir: null,
|
|
1635
|
+
backupDir: null
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
logger.log(`[docdex] Install outcome: ${local.outcome}`);
|
|
1639
|
+
const outcomeCode = local.outcome === "repair" ? "repair" : "replace";
|
|
1640
|
+
emitInstallerEvent(logger, {
|
|
1641
|
+
code: "DOCDEX_INSTALL_OUTCOME",
|
|
1642
|
+
details: {
|
|
1643
|
+
outcome: local.outcome,
|
|
1644
|
+
outcomeCode,
|
|
1645
|
+
reason: local.reason,
|
|
1646
|
+
downloadAttempted
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
return { binaryPath, outcome: local.outcome, outcomeCode };
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
let rollbackStatus = "not needed";
|
|
1652
|
+
let rollbackSucceeded = false;
|
|
1653
|
+
try {
|
|
1654
|
+
if (backupMoved && existsSync && existsSync(backupDir)) {
|
|
1655
|
+
if (existsSync(distDir)) {
|
|
1656
|
+
await fsModule.promises.rm(distDir, { recursive: true, force: true });
|
|
1657
|
+
}
|
|
1658
|
+
await fsModule.promises.rename(backupDir, distDir);
|
|
1659
|
+
rollbackStatus = "restored previous installation";
|
|
1660
|
+
rollbackSucceeded = true;
|
|
1661
|
+
} else if (promoted && !backupMoved) {
|
|
1662
|
+
if (existsSync && existsSync(distDir)) {
|
|
1663
|
+
await fsModule.promises.rm(distDir, { recursive: true, force: true });
|
|
1664
|
+
}
|
|
1665
|
+
rollbackStatus = "removed partial installation";
|
|
1666
|
+
}
|
|
1667
|
+
} catch {
|
|
1668
|
+
rollbackStatus = "failed";
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
let cleaned = [];
|
|
1672
|
+
if (extractAttempted || backupMoved || promoted) {
|
|
1673
|
+
await removeDirSafe(fsModule, stagingDir);
|
|
1674
|
+
await removeDirSafe(fsModule, failedDir);
|
|
1675
|
+
cleaned = await cleanupInstallArtifacts({
|
|
1676
|
+
fsModule,
|
|
1677
|
+
pathModule,
|
|
1678
|
+
distBaseDir,
|
|
1679
|
+
distDir,
|
|
1680
|
+
platformKey,
|
|
1681
|
+
stagingDir: null,
|
|
1682
|
+
backupDir: null,
|
|
1683
|
+
preserveBackups: !rollbackSucceeded && rollbackStatus === "failed"
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
installSafety.rollback = rollbackStatus;
|
|
1688
|
+
if (rollbackStatus === "restored previous installation") {
|
|
1689
|
+
installSafety.status = "rolled_back";
|
|
1690
|
+
} else if (rollbackStatus === "removed partial installation") {
|
|
1691
|
+
installSafety.status = "partial_removed";
|
|
1692
|
+
} else if (rollbackStatus === "not needed") {
|
|
1693
|
+
installSafety.status = "no_rollback_needed";
|
|
1694
|
+
} else {
|
|
1695
|
+
installSafety.status = "failed";
|
|
1696
|
+
}
|
|
1697
|
+
installSafety.cleanup = cleaned.length ? `removed ${cleaned.length} artifacts` : "none";
|
|
1698
|
+
installSafety.priorRunnable.after = existsSync
|
|
1699
|
+
? existsSync(pathModule.join(distDir, isWin32 ? "docdexd.exe" : "docdexd"))
|
|
1700
|
+
: null;
|
|
1701
|
+
|
|
1702
|
+
if (err && typeof err === "object") {
|
|
1703
|
+
err.details = {
|
|
1704
|
+
...withBaseDetails(err.details),
|
|
1705
|
+
installAttempt,
|
|
1706
|
+
installSafety
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
throw err;
|
|
1710
|
+
} finally {
|
|
1711
|
+
await fsModule.promises.rm(tmpFile, { force: true }).catch(() => {});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
84
1715
|
async function main() {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
1716
|
+
const result = await runInstaller();
|
|
1717
|
+
try {
|
|
1718
|
+
await runPostInstallSetup({ binaryPath: result?.binaryPath });
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
console.warn(`[docdex] postinstall setup skipped: ${err?.message || err}`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function appendInstallSafetyLines(lines, err) {
|
|
1725
|
+
const safety = err?.details?.installSafety;
|
|
1726
|
+
if (!safety) return lines;
|
|
1727
|
+
|
|
1728
|
+
const priorBefore = safety.priorRunnable?.before;
|
|
1729
|
+
const priorAfter = safety.priorRunnable?.after;
|
|
1730
|
+
|
|
1731
|
+
lines.push(`[docdex] Install safety status: ${safety.status || "unknown"}`);
|
|
1732
|
+
if (safety.preflightRecovery) {
|
|
1733
|
+
lines.push(`[docdex] Preflight recovery: ${safety.preflightRecovery}`);
|
|
1734
|
+
}
|
|
1735
|
+
if (safety.rollback) {
|
|
1736
|
+
lines.push(`[docdex] Rollback: ${safety.rollback}`);
|
|
1737
|
+
}
|
|
1738
|
+
if (priorBefore != null) {
|
|
1739
|
+
lines.push(`[docdex] Prior docdexd runnable at start: ${priorBefore ? "yes" : "no"}`);
|
|
1740
|
+
}
|
|
1741
|
+
if (priorAfter != null) {
|
|
1742
|
+
lines.push(`[docdex] Prior docdexd runnable after failure: ${priorAfter ? "yes" : "no"}`);
|
|
1743
|
+
}
|
|
1744
|
+
if (safety.cleanup) {
|
|
1745
|
+
lines.push(`[docdex] Cleanup: ${safety.cleanup}`);
|
|
1746
|
+
}
|
|
1747
|
+
return lines;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function describeFatalError(err) {
|
|
1751
|
+
const fallbackAttempted =
|
|
1752
|
+
err && typeof err.details?.fallbackAttempted === "boolean" ? err.details.fallbackAttempted : null;
|
|
1753
|
+
|
|
1754
|
+
if (err instanceof UnsupportedPlatformError) {
|
|
1755
|
+
const detected = `${err.details.platform}/${err.details.arch}`;
|
|
1756
|
+
const supportedKeys = (err.details.supportedPlatformKeys || []).join(", ");
|
|
1757
|
+
const supportedTriples = (err.details.supportedTargetTriples || []).join(", ");
|
|
1758
|
+
const libc = err.details?.libc ? String(err.details.libc) : null;
|
|
1759
|
+
const candidatePlatformKey =
|
|
1760
|
+
typeof err.details?.candidatePlatformKey === "string" ? err.details.candidatePlatformKey : null;
|
|
1761
|
+
const candidateTargetTriple =
|
|
1762
|
+
typeof err.details?.candidateTargetTriple === "string" ? err.details.candidateTargetTriple : null;
|
|
1763
|
+
const unpublished = err.details?.reason === "target_not_published";
|
|
1764
|
+
const candidateAssetPattern = candidatePlatformKey ? assetPatternForPlatformKey(candidatePlatformKey) : null;
|
|
1765
|
+
|
|
1766
|
+
return {
|
|
1767
|
+
code: err.code,
|
|
1768
|
+
exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
|
|
1769
|
+
details: withBaseDetails(err.details),
|
|
1770
|
+
lines: appendInstallSafetyLines(
|
|
1771
|
+
[
|
|
1772
|
+
`[docdex] install failed: unsupported platform (${detected})`,
|
|
1773
|
+
`[docdex] error code: ${err.code}`,
|
|
1774
|
+
"[docdex] No download was attempted for this platform.",
|
|
1775
|
+
libc ? `[docdex] Detected libc: ${libc}` : null,
|
|
1776
|
+
candidatePlatformKey ? `[docdex] Platform key: ${candidatePlatformKey}` : null,
|
|
1777
|
+
candidateTargetTriple ? `[docdex] Target triple: ${candidateTargetTriple}` : null,
|
|
1778
|
+
candidateAssetPattern ? `[docdex] Asset naming pattern: ${candidateAssetPattern}` : null,
|
|
1779
|
+
unpublished ? "[docdex] Note: this platform is recognized but no published binary is available yet." : null,
|
|
1780
|
+
supportedKeys ? `[docdex] Supported platforms: ${supportedKeys}` : null,
|
|
1781
|
+
supportedTriples ? `[docdex] Supported target triples: ${supportedTriples}` : null,
|
|
1782
|
+
"[docdex] Next steps:",
|
|
1783
|
+
"[docdex] - Use a supported platform (see list above).",
|
|
1784
|
+
"[docdex] - Or build from source (requires Rust): `cargo build --release --locked`.",
|
|
1785
|
+
"[docdex] - If you are on Linux and unsure of libc, set `DOCDEX_LIBC=gnu` or `DOCDEX_LIBC=musl`."
|
|
1786
|
+
].filter(Boolean),
|
|
1787
|
+
err
|
|
1788
|
+
)
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
if (err instanceof InstallerConfigError) {
|
|
1793
|
+
return {
|
|
1794
|
+
code: err.code,
|
|
1795
|
+
exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
|
|
1796
|
+
details: withBaseDetails(err.details),
|
|
1797
|
+
lines: appendInstallSafetyLines(
|
|
1798
|
+
[
|
|
1799
|
+
`[docdex] install failed: ${err.message}`,
|
|
1800
|
+
`[docdex] error code: ${err.code}`,
|
|
1801
|
+
"[docdex] Next steps:",
|
|
1802
|
+
"[docdex] - Ensure you are installing a published npm package version (not a local folder missing metadata).",
|
|
1803
|
+
"[docdex] - If installing from a fork, set `DOCDEX_DOWNLOAD_REPO=<owner/repo>` to the repo that hosts the release assets."
|
|
1804
|
+
],
|
|
1805
|
+
err
|
|
1806
|
+
)
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
if (err instanceof MissingArtifactError) {
|
|
1811
|
+
const detected = err.details?.detected ? `${err.details.detected.os}/${err.details.detected.arch}` : null;
|
|
1812
|
+
const platformKey = typeof err.details?.platformKey === "string" ? err.details.platformKey : null;
|
|
1813
|
+
const expectedAsset =
|
|
1814
|
+
typeof err.details?.expectedAsset === "string" && err.details.expectedAsset.trim()
|
|
1815
|
+
? err.details.expectedAsset.trim()
|
|
1816
|
+
: typeof err.details?.assetName === "string" && err.details.assetName.trim()
|
|
1817
|
+
? err.details.assetName.trim()
|
|
1818
|
+
: null;
|
|
1819
|
+
const expectedAssetPattern =
|
|
1820
|
+
typeof err.details?.expectedAssetPattern === "string" && err.details.expectedAssetPattern.trim()
|
|
1821
|
+
? err.details.expectedAssetPattern.trim()
|
|
1822
|
+
: assetPatternForPlatformKey(platformKey, { exampleAssetName: expectedAsset || undefined });
|
|
1823
|
+
return {
|
|
1824
|
+
code: err.code,
|
|
1825
|
+
exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
|
|
1826
|
+
details: withBaseDetails(err.details),
|
|
1827
|
+
lines: appendInstallSafetyLines(
|
|
1828
|
+
[
|
|
1829
|
+
"[docdex] install failed: missing artifact/version sync issue (release asset not found)",
|
|
1830
|
+
`[docdex] error code: ${err.code}`,
|
|
1831
|
+
detected ? `[docdex] Detected platform: ${detected}` : null,
|
|
1832
|
+
err.details?.platformKey ? `[docdex] Platform key: ${err.details.platformKey}` : null,
|
|
1833
|
+
err.details?.targetTriple ? `[docdex] Expected target triple: ${err.details.targetTriple}` : null,
|
|
1834
|
+
err.details?.manifestName ? `[docdex] Manifest name: ${err.details.manifestName}` : null,
|
|
1835
|
+
err.details?.manifestVersion != null ? `[docdex] Manifest version: ${err.details.manifestVersion}` : null,
|
|
1836
|
+
fallbackAttempted != null ? `[docdex] Fallback attempted: ${fallbackAttempted}` : null,
|
|
1837
|
+
err.details?.fallbackReason ? `[docdex] Fallback reason: ${err.details.fallbackReason}` : null,
|
|
1838
|
+
err.details?.version ? `[docdex] Version: v${err.details.version}` : null,
|
|
1839
|
+
err.details?.repoSlug ? `[docdex] Download repo: ${err.details.repoSlug}` : null,
|
|
1840
|
+
err.details?.expectedAsset ? `[docdex] Expected asset: ${err.details.expectedAsset}` : null,
|
|
1841
|
+
expectedAssetPattern ? `[docdex] Asset naming pattern: ${expectedAssetPattern}` : null,
|
|
1842
|
+
err.details?.downloadUrl ? `[docdex] URL tried: ${err.details.downloadUrl}` : null,
|
|
1843
|
+
err.details?.note ? `[docdex] Note: ${err.details.note}` : null,
|
|
1844
|
+
"[docdex] Next steps:",
|
|
1845
|
+
"[docdex] - Confirm the GitHub Release for this version contains the expected asset for your target.",
|
|
1846
|
+
"[docdex] - If installing from a fork, set `DOCDEX_DOWNLOAD_REPO=<owner/repo>` to the repo that hosts the assets.",
|
|
1847
|
+
"[docdex] - Workaround: install a version with matching assets, or build from source (`cargo build --release --locked`)."
|
|
1848
|
+
].filter(Boolean),
|
|
1849
|
+
err
|
|
1850
|
+
)
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
92
1853
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1854
|
+
if (err instanceof ChecksumResolutionError) {
|
|
1855
|
+
const checksumCandidates = Array.isArray(err.details?.checksumCandidates) ? err.details.checksumCandidates : [];
|
|
1856
|
+
return {
|
|
1857
|
+
code: err.code,
|
|
1858
|
+
exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
|
|
1859
|
+
details: withBaseDetails(err.details),
|
|
1860
|
+
lines: appendInstallSafetyLines(
|
|
1861
|
+
[
|
|
1862
|
+
`[docdex] install failed: ${err.message}`,
|
|
1863
|
+
`[docdex] error code: ${err.code}`,
|
|
1864
|
+
err.details?.assetName ? `[docdex] Asset: ${err.details.assetName}` : null,
|
|
1865
|
+
err.details?.targetTriple ? `[docdex] Expected target triple: ${err.details.targetTriple}` : null,
|
|
1866
|
+
err.details?.manifestName ? `[docdex] Manifest name: ${err.details.manifestName}` : null,
|
|
1867
|
+
err.details?.manifestVersion != null ? `[docdex] Manifest version: ${err.details.manifestVersion}` : null,
|
|
1868
|
+
checksumCandidates.length
|
|
1869
|
+
? `[docdex] Checksum candidates tried: ${checksumCandidates.join(", ")}`
|
|
1870
|
+
: null,
|
|
1871
|
+
err.details?.fallbackReason ? `[docdex] Fallback reason: ${err.details.fallbackReason}` : null,
|
|
1872
|
+
"[docdex] Next steps:",
|
|
1873
|
+
"[docdex] - Ensure the GitHub Release includes `docdex-release-manifest.json` or `SHA256SUMS` with a line for this asset.",
|
|
1874
|
+
"[docdex] - If installing from a fork, set `DOCDEX_DOWNLOAD_REPO=<owner/repo>` to the repo that hosts the release assets.",
|
|
1875
|
+
"[docdex] - If you cannot publish checksums, build from source (`cargo build --release --locked`)."
|
|
1876
|
+
].filter(Boolean),
|
|
1877
|
+
err
|
|
1878
|
+
)
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (err instanceof DownloadError) {
|
|
1883
|
+
return {
|
|
1884
|
+
code: err.code,
|
|
1885
|
+
exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
|
|
1886
|
+
details: withBaseDetails(err.details),
|
|
1887
|
+
lines: appendInstallSafetyLines(
|
|
1888
|
+
[
|
|
1889
|
+
`[docdex] install failed: ${err.message}`,
|
|
1890
|
+
`[docdex] error code: ${err.code}`,
|
|
1891
|
+
err.details?.downloadUrl ? `[docdex] URL tried: ${err.details.downloadUrl}` : null,
|
|
1892
|
+
err.details?.statusCode != null ? `[docdex] HTTP status: ${err.details.statusCode}` : null,
|
|
1893
|
+
err.cause?.message ? `[docdex] Cause: ${err.cause.message}` : null
|
|
1894
|
+
].filter(Boolean),
|
|
1895
|
+
err
|
|
1896
|
+
)
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
if (err instanceof IntegrityMismatchError) {
|
|
1901
|
+
const expectedSha256 = typeof err.details?.expectedSha256 === "string" ? err.details.expectedSha256 : null;
|
|
1902
|
+
const actualSha256 = typeof err.details?.actualSha256 === "string" ? err.details.actualSha256 : null;
|
|
1903
|
+
return {
|
|
1904
|
+
code: err.code,
|
|
1905
|
+
exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
|
|
1906
|
+
details: withBaseDetails(err.details),
|
|
1907
|
+
lines: appendInstallSafetyLines(
|
|
1908
|
+
[
|
|
1909
|
+
`[docdex] install failed: ${err.message}`,
|
|
1910
|
+
`[docdex] error code: ${err.code}`,
|
|
1911
|
+
err.details?.assetName ? `[docdex] Asset: ${err.details.assetName}` : null,
|
|
1912
|
+
err.details?.downloadUrl ? `[docdex] URL tried: ${err.details.downloadUrl}` : null,
|
|
1913
|
+
expectedSha256 ? `[docdex] Expected sha256: ${expectedSha256}` : null,
|
|
1914
|
+
actualSha256 ? `[docdex] Actual sha256: ${actualSha256}` : null,
|
|
1915
|
+
err.details?.source ? `[docdex] Source: ${err.details.source}` : null,
|
|
1916
|
+
err.details?.manifestName ? `[docdex] Manifest name: ${err.details.manifestName}` : null,
|
|
1917
|
+
err.details?.manifestVersion != null ? `[docdex] Manifest version: ${err.details.manifestVersion}` : null,
|
|
1918
|
+
fallbackAttempted != null ? `[docdex] Fallback attempted: ${fallbackAttempted}` : null,
|
|
1919
|
+
"[docdex] Next steps:",
|
|
1920
|
+
"[docdex] - Re-run the install; transient network/caching issues can corrupt downloads.",
|
|
1921
|
+
"[docdex] - Ensure you are installing from the intended repo/version (DOCDEX_DOWNLOAD_REPO, DOCDEX_VERSION).",
|
|
1922
|
+
"[docdex] - If behind a proxy or cache, bypass it; integrity mismatches can indicate tampering.",
|
|
1923
|
+
"[docdex] - If it still fails, build from source (`cargo build --release --locked`)."
|
|
1924
|
+
].filter(Boolean),
|
|
1925
|
+
err
|
|
1926
|
+
)
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
97
1929
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
1930
|
+
if (err instanceof ArchiveInvalidError) {
|
|
1931
|
+
return {
|
|
1932
|
+
code: err.code,
|
|
1933
|
+
exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
|
|
1934
|
+
details: withBaseDetails(err.details),
|
|
1935
|
+
lines: appendInstallSafetyLines(
|
|
1936
|
+
[
|
|
1937
|
+
`[docdex] install failed: ${err.message}`,
|
|
1938
|
+
`[docdex] error code: ${err.code}`,
|
|
1939
|
+
err.details?.binaryPath ? `[docdex] Expected binary path: ${err.details.binaryPath}` : null
|
|
1940
|
+
].filter(Boolean),
|
|
1941
|
+
err
|
|
1942
|
+
)
|
|
1943
|
+
};
|
|
101
1944
|
}
|
|
102
1945
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1946
|
+
if (err instanceof ManifestResolutionError) {
|
|
1947
|
+
const platformKey = typeof err.details?.platformKey === "string" ? err.details.platformKey : null;
|
|
1948
|
+
const expectedAssetPattern =
|
|
1949
|
+
typeof err.details?.expectedAssetPattern === "string"
|
|
1950
|
+
? err.details.expectedAssetPattern
|
|
1951
|
+
: platformKey
|
|
1952
|
+
? assetPatternForPlatformKey(platformKey)
|
|
1953
|
+
: assetPatternForPlatformKey(null);
|
|
1954
|
+
|
|
1955
|
+
const lines =
|
|
1956
|
+
err.code === "DOCDEX_ASSET_NO_MATCH"
|
|
1957
|
+
? [
|
|
1958
|
+
"[docdex] install failed: missing artifact/version sync issue (manifest has no asset for this target)",
|
|
1959
|
+
`[docdex] error code: ${err.code}`,
|
|
1960
|
+
err.details?.targetTriple ? `[docdex] Expected target triple: ${err.details.targetTriple}` : null,
|
|
1961
|
+
`[docdex] Asset naming pattern: ${expectedAssetPattern}`,
|
|
1962
|
+
`[docdex] Details: ${err.message}`
|
|
1963
|
+
].filter(Boolean)
|
|
1964
|
+
: [`[docdex] install failed: ${err.message}`, `[docdex] error code: ${err.code}`];
|
|
1965
|
+
|
|
1966
|
+
if (fallbackAttempted === false) {
|
|
1967
|
+
lines.push("[docdex] Fallback was not attempted because a manifest was present but unusable.");
|
|
1968
|
+
}
|
|
1969
|
+
if (Array.isArray(err.details?.supported) && err.details.supported.length) {
|
|
1970
|
+
lines.push(`[docdex] supported targets: ${err.details.supported.join(", ")}`);
|
|
1971
|
+
}
|
|
1972
|
+
if (Array.isArray(err.details?.matches) && err.details.matches.length) {
|
|
1973
|
+
lines.push(`[docdex] matched assets: ${err.details.matches.join(", ")}`);
|
|
1974
|
+
}
|
|
1975
|
+
return {
|
|
1976
|
+
code: err.code,
|
|
1977
|
+
exitCode: err.exitCode || EXIT_CODE_BY_ERROR_CODE[err.code] || 1,
|
|
1978
|
+
details: withBaseDetails(err.details),
|
|
1979
|
+
lines: appendInstallSafetyLines(lines, err)
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
const code = (err && typeof err.code === "string" && err.code) || "DOCDEX_INSTALL_FAILED";
|
|
1984
|
+
return {
|
|
1985
|
+
code,
|
|
1986
|
+
exitCode: (err && typeof err.exitCode === "number" && err.exitCode) || EXIT_CODE_BY_ERROR_CODE[code] || 1,
|
|
1987
|
+
details: withBaseDetails(err && err.details),
|
|
1988
|
+
lines: appendInstallSafetyLines(
|
|
1989
|
+
[`[docdex] install failed: ${err?.message || "unknown error"}`, `[docdex] error code: ${code}`],
|
|
1990
|
+
err
|
|
1991
|
+
)
|
|
1992
|
+
};
|
|
106
1993
|
}
|
|
107
1994
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
1995
|
+
function handleFatal(err) {
|
|
1996
|
+
const report = describeFatalError(err);
|
|
1997
|
+
for (const line of report.lines) console.error(line);
|
|
1998
|
+
process.exit(report.exitCode || 1);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if (require.main === module) {
|
|
2002
|
+
main().catch(handleFatal);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
module.exports = {
|
|
2006
|
+
checksumCandidateNames,
|
|
2007
|
+
manifestCandidateNames,
|
|
2008
|
+
tryResolveAssetViaManifest,
|
|
2009
|
+
tryResolveSha256ViaChecksumFiles,
|
|
2010
|
+
resolveInstallerDownloadPlan,
|
|
2011
|
+
parseSha256File,
|
|
2012
|
+
sha256File,
|
|
2013
|
+
recoverInterruptedInstall,
|
|
2014
|
+
verifyInstalledDocdexdIntegrity,
|
|
2015
|
+
decideInstallAction,
|
|
2016
|
+
determineLocalInstallerOutcome,
|
|
2017
|
+
verifyDownloadedFileIntegrity,
|
|
2018
|
+
MissingArtifactError,
|
|
2019
|
+
ChecksumResolutionError,
|
|
2020
|
+
runInstaller,
|
|
2021
|
+
describeFatalError,
|
|
2022
|
+
handleFatal
|
|
2023
|
+
};
|