pi-courier 0.1.37 → 0.1.39
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/README.md +56 -3
- package/README.zh-CN.md +56 -3
- package/dist/auth/headless-login.d.ts +140 -0
- package/dist/auth/headless-login.d.ts.map +1 -0
- package/dist/auth/headless-login.js +386 -0
- package/dist/auth/headless-login.js.map +1 -0
- package/dist/quote-cache.d.ts +35 -0
- package/dist/quote-cache.d.ts.map +1 -0
- package/dist/quote-cache.js +51 -0
- package/dist/quote-cache.js.map +1 -0
- package/dist/rpc/command-map.d.ts +63 -0
- package/dist/rpc/command-map.d.ts.map +1 -1
- package/dist/rpc/command-map.js +327 -3
- package/dist/rpc/command-map.js.map +1 -1
- package/dist/rpc/message-router.d.ts +67 -2
- package/dist/rpc/message-router.d.ts.map +1 -1
- package/dist/rpc/message-router.js +297 -3
- package/dist/rpc/message-router.js.map +1 -1
- package/dist/rpc/pi-rpc.d.ts +64 -3
- package/dist/rpc/pi-rpc.d.ts.map +1 -1
- package/dist/rpc/pi-rpc.js +73 -16
- package/dist/rpc/pi-rpc.js.map +1 -1
- package/dist/rpc/project-manager.d.ts +5 -0
- package/dist/rpc/project-manager.d.ts.map +1 -1
- package/dist/rpc/project-manager.js +7 -0
- package/dist/rpc/project-manager.js.map +1 -1
- package/dist/transports/matrix.d.ts +2 -0
- package/dist/transports/matrix.d.ts.map +1 -1
- package/dist/transports/matrix.js +17 -0
- package/dist/transports/matrix.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/crypto-native-hashes.json +3 -0
- package/scripts/ensure-crypto-native.d.mts +84 -1
- package/scripts/ensure-crypto-native.mjs +338 -10
|
@@ -18,15 +18,55 @@
|
|
|
18
18
|
* present before first run. Any failure here degrades to a warning; it never
|
|
19
19
|
* fails the install (the bridge can still run without E2EE crypto).
|
|
20
20
|
*
|
|
21
|
+
* LOCAL CACHE + SHA256 VERIFICATION (issue #48):
|
|
22
|
+
*
|
|
23
|
+
* Every `npm update` reinstalls the dependency tree and wipes the freshly
|
|
24
|
+
* downloaded .node, forcing a ~21MB re-download (≈1min behind slow proxies).
|
|
25
|
+
* To avoid that, a verified copy is kept under
|
|
26
|
+
* `$XDG_CACHE_HOME/pi-courier/native-crypto/<crypto version>/<basename>.node`
|
|
27
|
+
* (default `~/.cache/pi-courier/native-crypto/...`) with a `<basename>.sha256`
|
|
28
|
+
* sidecar recording the observed digest. When a later install finds the
|
|
29
|
+
* binding missing, the cache is consulted first (sidecar + manifest both
|
|
30
|
+
* verified before use) and restored with zero network traffic.
|
|
31
|
+
*
|
|
32
|
+
* Integrity: binaries are loaded by Node as native code, so a tampered
|
|
33
|
+
* download is arbitrary code execution. Every accepted binary's sha256 must
|
|
34
|
+
* match `scripts/crypto-native-hashes.json` (keyed `<version>/<basename>`).
|
|
35
|
+
* A listed version with a mismatching digest is DELETED and E2EE degrades —
|
|
36
|
+
* the binary is never loaded. An unlisted version is accepted TOFU-style with
|
|
37
|
+
* a loud warning asking the maintainer to record the digest. Cache entries
|
|
38
|
+
* whose sidecar disagrees with their bytes are treated as absent.
|
|
39
|
+
*
|
|
40
|
+
* All functions are pure-ish (explicit path/manifest params, injectable
|
|
41
|
+
* downloader) so tests can exercise every branch in temp dirs without network.
|
|
42
|
+
*
|
|
21
43
|
* Usage (from package root): node scripts/ensure-crypto-native.mjs
|
|
22
44
|
*/
|
|
23
45
|
|
|
24
46
|
import { execFileSync } from "node:child_process";
|
|
25
|
-
import {
|
|
47
|
+
import { createHash } from "node:crypto";
|
|
48
|
+
import {
|
|
49
|
+
chmodSync,
|
|
50
|
+
copyFileSync,
|
|
51
|
+
existsSync,
|
|
52
|
+
mkdirSync,
|
|
53
|
+
readFileSync,
|
|
54
|
+
renameSync,
|
|
55
|
+
rmSync,
|
|
56
|
+
writeFileSync,
|
|
57
|
+
} from "node:fs";
|
|
26
58
|
import { createRequire } from "node:module";
|
|
59
|
+
import { homedir } from "node:os";
|
|
60
|
+
import { join } from "node:path";
|
|
61
|
+
import { fileURLToPath } from "node:url";
|
|
27
62
|
|
|
28
63
|
const require = createRequire(import.meta.url);
|
|
29
64
|
|
|
65
|
+
const LOG_PREFIX = "[pi-courier]";
|
|
66
|
+
const CACHE_SUBDIR = join("pi-courier", "native-crypto");
|
|
67
|
+
const MANIFEST_FILENAME = "crypto-native-hashes.json";
|
|
68
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
69
|
+
|
|
30
70
|
/** Current platform/arch names as napi-rs names them (no .node suffix). */
|
|
31
71
|
export function nativeBindingBasename() {
|
|
32
72
|
const { platform, arch } = process;
|
|
@@ -108,40 +148,328 @@ export function runNativeDownloader(cryptoDir) {
|
|
|
108
148
|
});
|
|
109
149
|
}
|
|
110
150
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Local cache + sha256 manifest (issue #48)
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/** Cache root: $XDG_CACHE_HOME/pi-courier/native-crypto, else ~/.cache/… */
|
|
156
|
+
export function defaultCacheRoot(env = process.env, home = homedir()) {
|
|
157
|
+
const xdg = env.XDG_CACHE_HOME;
|
|
158
|
+
const base = typeof xdg === "string" && xdg.trim() !== "" ? xdg : join(home, ".cache");
|
|
159
|
+
return join(base, CACHE_SUBDIR);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Manifest ships next to this script (package.json `files` covers scripts/). */
|
|
163
|
+
export function defaultManifestPath() {
|
|
164
|
+
return fileURLToPath(new URL(`./${MANIFEST_FILENAME}`, import.meta.url));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Cache layout: `<cacheRoot>/<version>/<basename>.node` + `<basename>.sha256`. */
|
|
168
|
+
export function cacheEntryPaths(cacheRoot, version, basename) {
|
|
169
|
+
const dir = join(cacheRoot, version);
|
|
170
|
+
const nodePath = join(dir, `${basename}.node`);
|
|
171
|
+
return { dir, nodePath, shaPath: join(dir, `${basename}.sha256`) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** sha256 of a file's bytes, lowercase hex. */
|
|
175
|
+
export function sha256FileSync(filePath) {
|
|
176
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The crypto package's own version (cache namespace), or null if unreadable. */
|
|
180
|
+
export function readCryptoPackageVersion(cryptoDir) {
|
|
181
|
+
try {
|
|
182
|
+
const pkg = JSON.parse(readFileSync(`${cryptoDir}/package.json`, "utf8"));
|
|
183
|
+
return typeof pkg.version === "string" && pkg.version !== "" ? pkg.version : null;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Read the sha256 manifest. Any problem (missing, malformed, wrong shape)
|
|
191
|
+
* degrades to an empty manifest — i.e. "nothing is listed", which makes the
|
|
192
|
+
* flow take the TOFU path instead of ever throwing during install.
|
|
193
|
+
*/
|
|
194
|
+
export function readManifest(manifestPath) {
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
198
|
+
} catch (err) {
|
|
199
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
200
|
+
console.warn(
|
|
201
|
+
`${LOG_PREFIX} crypto hash manifest unreadable at ${manifestPath} (${message}) — ` +
|
|
202
|
+
`treating every version as unlisted (TOFU).`
|
|
203
|
+
);
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
207
|
+
console.warn(
|
|
208
|
+
`${LOG_PREFIX} crypto hash manifest malformed at ${manifestPath} — ` +
|
|
209
|
+
`treating every version as unlisted (TOFU).`
|
|
210
|
+
);
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
return parsed;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function manifestLookup(manifest, key) {
|
|
217
|
+
if (key === null) return { listed: false };
|
|
218
|
+
if (!Object.prototype.hasOwnProperty.call(manifest, key)) return { listed: false };
|
|
219
|
+
const expected = String(manifest[key]).trim().toLowerCase();
|
|
220
|
+
return { listed: true, expected: SHA256_HEX.test(expected) ? expected : null };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function manifestNag(key, observedSha256) {
|
|
224
|
+
console.warn(
|
|
225
|
+
`${LOG_PREFIX} crypto binding ${key} is NOT in the sha256 manifest ` +
|
|
226
|
+
`(scripts/${MANIFEST_FILENAME}, observed sha256 ${observedSha256}) — ` +
|
|
227
|
+
`accepted this once (TOFU). ` +
|
|
228
|
+
`Please record that digest so future installs verify against it.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** tmp path for an atomic write; process-unique and random to survive races. */
|
|
233
|
+
function tmpPathFor(destPath) {
|
|
234
|
+
return `${destPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Write via temp file + atomic rename so an interrupt never leaves half a file. */
|
|
238
|
+
function atomicWrite(destPath, write, mode) {
|
|
239
|
+
const tmp = tmpPathFor(destPath);
|
|
240
|
+
try {
|
|
241
|
+
write(tmp);
|
|
242
|
+
chmodSync(tmp, mode);
|
|
243
|
+
renameSync(tmp, destPath);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
try {
|
|
246
|
+
rmSync(tmp, { force: true });
|
|
247
|
+
} catch {
|
|
248
|
+
// best effort — the tmp residue is inert
|
|
249
|
+
}
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Copy a verified .node into the cache: `<version>/<basename>.node` plus a
|
|
256
|
+
* `<basename>.sha256` sidecar with the observed digest. Dir 0700, files 0600,
|
|
257
|
+
* both written atomically (sidecar last: a torn write at worst drops the
|
|
258
|
+
* sidecar, and a cache entry without a sidecar is treated as absent).
|
|
259
|
+
*/
|
|
260
|
+
export function writeCacheEntry(cacheRoot, version, basename, sourceNodePath, observedSha256) {
|
|
261
|
+
const { dir, nodePath, shaPath } = cacheEntryPaths(cacheRoot, version, basename);
|
|
262
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
263
|
+
chmodSync(dir, 0o700); // mkdir mode is umask-filtered; pin it explicitly
|
|
264
|
+
atomicWrite(nodePath, (tmp) => copyFileSync(sourceNodePath, tmp), 0o600);
|
|
265
|
+
atomicWrite(shaPath, (tmp) => writeFileSync(tmp, `${observedSha256}\n`), 0o600);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Evaluate the cache entry for `<version>/<basename>` without using it yet.
|
|
270
|
+
*
|
|
271
|
+
* A hit requires: the .node exists, its sha256 matches the sidecar, AND (when
|
|
272
|
+
* the version is listed in the manifest) the manifest digest too. Anything
|
|
273
|
+
* else is a miss — the cached copy is not used but also not deleted here.
|
|
274
|
+
* An unlisted-but-self-consistent entry TOFU-hits with a warning (≤1 per run:
|
|
275
|
+
* this is called at most once per install).
|
|
276
|
+
*/
|
|
277
|
+
export function readCacheEntry(cacheRoot, version, basename, manifest) {
|
|
278
|
+
const { nodePath, shaPath } = cacheEntryPaths(cacheRoot, version, basename);
|
|
279
|
+
if (!existsSync(nodePath)) {
|
|
280
|
+
return { hit: false, reason: "absent" };
|
|
281
|
+
}
|
|
282
|
+
let observed;
|
|
283
|
+
try {
|
|
284
|
+
observed = sha256FileSync(nodePath);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
287
|
+
console.warn(`${LOG_PREFIX} could not hash cached crypto binding ${nodePath}: ${message}`);
|
|
288
|
+
return { hit: false, reason: "unreadable" };
|
|
289
|
+
}
|
|
290
|
+
let sidecar;
|
|
291
|
+
try {
|
|
292
|
+
sidecar = readFileSync(shaPath, "utf8").trim().toLowerCase();
|
|
293
|
+
} catch {
|
|
294
|
+
return { hit: false, reason: "sidecar-missing" };
|
|
295
|
+
}
|
|
296
|
+
if (!SHA256_HEX.test(sidecar) || sidecar !== observed) {
|
|
297
|
+
return { hit: false, reason: "sidecar-mismatch", observed };
|
|
298
|
+
}
|
|
299
|
+
const key = `${version}/${basename}`;
|
|
300
|
+
const listed = manifestLookup(manifest, key);
|
|
301
|
+
if (listed.listed) {
|
|
302
|
+
if (listed.expected === null || listed.expected !== observed) {
|
|
303
|
+
return { hit: false, reason: "manifest-mismatch", observed };
|
|
304
|
+
}
|
|
305
|
+
return { hit: true, nodePath, sha256: observed, verified: "manifest" };
|
|
306
|
+
}
|
|
307
|
+
manifestNag(key, observed);
|
|
308
|
+
return { hit: true, nodePath, sha256: observed, verified: "tofu" };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Atomically copy a verified cache entry back into the crypto package dir. */
|
|
312
|
+
export function restoreBindingFromCache(cryptoDir, basename, cacheNodePath) {
|
|
313
|
+
const target = `${cryptoDir}/${basename}.node`;
|
|
314
|
+
atomicWrite(target, (tmp) => copyFileSync(cacheNodePath, tmp), 0o644);
|
|
315
|
+
return target;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Verify a freshly downloaded binding against the manifest. Pure verdict: the
|
|
320
|
+
* caller decides what to delete. Verdicts:
|
|
321
|
+
* manifest-match digest matches the manifest — accept.
|
|
322
|
+
* manifest-mismatch listed digest differs — caller must DELETE the artifact
|
|
323
|
+
* and degrade E2EE; the binary must never be loaded.
|
|
324
|
+
* manifest-miss version not listed — TOFU accept; caller warns.
|
|
325
|
+
*/
|
|
326
|
+
export function verifyDownloadedBinding(nodePath, manifestKey, manifest) {
|
|
327
|
+
const observed = sha256FileSync(nodePath);
|
|
328
|
+
const listed = manifestLookup(manifest, manifestKey);
|
|
329
|
+
if (!listed.listed) {
|
|
330
|
+
return { verdict: "manifest-miss", observed, expected: null };
|
|
331
|
+
}
|
|
332
|
+
if (listed.expected === null || listed.expected !== observed) {
|
|
333
|
+
return { verdict: "manifest-mismatch", observed, expected: listed.expected };
|
|
334
|
+
}
|
|
335
|
+
return { verdict: "manifest-match", observed, expected: listed.expected };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Orchestration. All inputs are injectable so tests run in temp dirs; the
|
|
340
|
+
* no-argument call keeps the exact pre-cache behaviour contract:
|
|
341
|
+
* `{ok, downloaded, reason?}` with failures warned, never thrown.
|
|
342
|
+
*
|
|
343
|
+
* @param {{
|
|
344
|
+
* cryptoDir?: string | null,
|
|
345
|
+
* basename?: string | null,
|
|
346
|
+
* cacheRoot?: string,
|
|
347
|
+
* manifestPath?: string,
|
|
348
|
+
* runDownloader?: (cryptoDir: string) => void,
|
|
349
|
+
* }} [options]
|
|
350
|
+
*/
|
|
351
|
+
export function ensureCryptoNative(options = {}) {
|
|
352
|
+
const {
|
|
353
|
+
cryptoDir = resolveCryptoPackageDir(),
|
|
354
|
+
basename = nativeBindingBasename(),
|
|
355
|
+
cacheRoot = defaultCacheRoot(),
|
|
356
|
+
manifestPath = defaultManifestPath(),
|
|
357
|
+
runDownloader = runNativeDownloader,
|
|
358
|
+
} = options;
|
|
114
359
|
|
|
115
360
|
if (!cryptoDir) {
|
|
116
361
|
console.warn(
|
|
117
|
-
|
|
362
|
+
`${LOG_PREFIX} crypto package not found — can't verify native binding; continuing (E2EE may be unavailable).`
|
|
118
363
|
);
|
|
119
364
|
return { ok: false, reason: "crypto-package-not-found" };
|
|
120
365
|
}
|
|
121
366
|
|
|
122
367
|
if (!isNativeBindingMissing(cryptoDir, basename)) {
|
|
123
368
|
// Already present (either downloaded once, or platform unsupported).
|
|
369
|
+
// Deliberately no post-hoc verification of an existing binding.
|
|
124
370
|
return { ok: true, downloaded: false };
|
|
125
371
|
}
|
|
126
372
|
|
|
373
|
+
const version = readCryptoPackageVersion(cryptoDir);
|
|
374
|
+
const manifest = readManifest(manifestPath);
|
|
375
|
+
const key = version !== null && basename !== null ? `${version}/${basename}` : null;
|
|
376
|
+
|
|
377
|
+
// 1) Warm cache: restore a verified copy without touching the network.
|
|
378
|
+
if (key !== null) {
|
|
379
|
+
let entry;
|
|
380
|
+
try {
|
|
381
|
+
entry = readCacheEntry(cacheRoot, version, basename, manifest);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
384
|
+
console.warn(`${LOG_PREFIX} cache lookup failed: ${message} — falling back to download.`);
|
|
385
|
+
entry = { hit: false };
|
|
386
|
+
}
|
|
387
|
+
if (entry.hit) {
|
|
388
|
+
try {
|
|
389
|
+
restoreBindingFromCache(cryptoDir, basename, entry.nodePath);
|
|
390
|
+
console.info(
|
|
391
|
+
`${LOG_PREFIX} crypto native binding (${basename}.node) restored from local cache ` +
|
|
392
|
+
`(${key}, sha256 ${entry.sha256}) — no download needed.`
|
|
393
|
+
);
|
|
394
|
+
return { ok: true, downloaded: false, restoredFromCache: true, verified: entry.verified };
|
|
395
|
+
} catch (err) {
|
|
396
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
397
|
+
console.warn(
|
|
398
|
+
`${LOG_PREFIX} could not restore crypto binding from cache: ${message} — falling back to download.`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 2) Cache miss: run the upstream downloader (inherits proxy env).
|
|
127
405
|
console.info(
|
|
128
|
-
|
|
406
|
+
`${LOG_PREFIX} crypto native binding (${basename}.node) missing — downloading (POSTINSTALL due to npm 11 allow-scripts).`
|
|
129
407
|
);
|
|
130
408
|
try {
|
|
131
|
-
|
|
132
|
-
return { ok: true, downloaded: true };
|
|
409
|
+
runDownloader(cryptoDir);
|
|
133
410
|
} catch (err) {
|
|
134
411
|
const message = err instanceof Error ? err.message : String(err);
|
|
135
412
|
console.warn(
|
|
136
|
-
|
|
413
|
+
`${LOG_PREFIX} could not auto-download crypto native binding: ${message}\n` +
|
|
137
414
|
` The bridge may still run without E2EE. To install manually:\n` +
|
|
138
415
|
` cd ${cryptoDir} && node download-lib.js`
|
|
139
416
|
);
|
|
140
417
|
return { ok: false, reason: "download-failed" };
|
|
141
418
|
}
|
|
419
|
+
|
|
420
|
+
// 3) Verify the downloaded binary before accepting it (or caching it).
|
|
421
|
+
const nodePath = `${cryptoDir}/${basename}.node`;
|
|
422
|
+
let check;
|
|
423
|
+
try {
|
|
424
|
+
check = verifyDownloadedBinding(nodePath, key, manifest);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
427
|
+
console.warn(
|
|
428
|
+
`${LOG_PREFIX} could not verify downloaded crypto binding (${nodePath}): ${message} — ` +
|
|
429
|
+
`continuing without it (E2EE may be unavailable).`
|
|
430
|
+
);
|
|
431
|
+
return { ok: false, reason: "verify-failed" };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (check.verdict === "manifest-mismatch") {
|
|
435
|
+
// Never load an unverified native binary. Delete and degrade.
|
|
436
|
+
try {
|
|
437
|
+
rmSync(nodePath, { force: true });
|
|
438
|
+
} catch (err) {
|
|
439
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
440
|
+
console.warn(`${LOG_PREFIX} could not delete the unverified binding: ${message}`);
|
|
441
|
+
}
|
|
442
|
+
console.warn(
|
|
443
|
+
`${LOG_PREFIX} downloaded crypto binding ${key} FAILED sha256 verification ` +
|
|
444
|
+
`(expected ${check.expected}, observed ${check.observed}) — file deleted, ` +
|
|
445
|
+
`continuing WITHOUT native E2EE crypto (the bridge still runs, encrypted ` +
|
|
446
|
+
`rooms will be unreadable). Possible tampering or upstream asset change.`
|
|
447
|
+
);
|
|
448
|
+
return { ok: false, reason: "hash-mismatch", verified: "rejected" };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (check.verdict === "manifest-miss" && key !== null) {
|
|
452
|
+
manifestNag(key, check.observed);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 4) Best-effort: stash the verified copy for future installs.
|
|
456
|
+
if (key !== null) {
|
|
457
|
+
try {
|
|
458
|
+
writeCacheEntry(cacheRoot, version, basename, nodePath, check.observed);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
461
|
+
console.warn(`${LOG_PREFIX} could not cache the crypto binding: ${message}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
ok: true,
|
|
467
|
+
downloaded: true,
|
|
468
|
+
verified: check.verdict === "manifest-match" ? "manifest" : "tofu",
|
|
469
|
+
};
|
|
142
470
|
}
|
|
143
471
|
|
|
144
472
|
// Allow running both as a bin and importing the named exports for tests.
|
|
145
473
|
if (process.argv[1] && process.argv[1].endsWith("ensure-crypto-native.mjs")) {
|
|
146
474
|
ensureCryptoNative();
|
|
147
|
-
}
|
|
475
|
+
}
|