fallow 2.81.0 → 2.83.0

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.
@@ -24,13 +24,13 @@
24
24
  //
25
25
  // No external dependencies: uses node:crypto and node:fs only.
26
26
 
27
- const crypto = require('node:crypto');
28
- const fs = require('node:fs');
29
- const https = require('node:https');
30
- const path = require('node:path');
31
- const { getPlatformPackage } = require('./platform-package');
27
+ const crypto = require("node:crypto");
28
+ const fs = require("node:fs");
29
+ const https = require("node:https");
30
+ const path = require("node:path");
31
+ const { getPlatformPackage } = require("./platform-package");
32
32
 
33
- const GITHUB_REPO = 'fallow-rs/fallow';
33
+ const GITHUB_REPO = "fallow-rs/fallow";
34
34
  const DIGEST_TIMEOUT_MS = 10000;
35
35
 
36
36
  // 32-byte Ed25519 public key, identical to BINARY_SIGNING_PUBLIC_KEY in
@@ -39,8 +39,8 @@ const DIGEST_TIMEOUT_MS = 10000;
39
39
  // works offline and cannot be silently downgraded by tampering with the network
40
40
  // path.
41
41
  const EMBEDDED_PUBLIC_KEY = Buffer.from([
42
- 131, 78, 111, 215, 115, 51, 230, 238, 223, 119, 147, 71, 199, 16, 172, 180, 3, 210, 216, 35,
43
- 77, 85, 159, 94, 215, 200, 126, 85, 42, 222, 11, 209,
42
+ 131, 78, 111, 215, 115, 51, 230, 238, 223, 119, 147, 71, 199, 16, 172, 180, 3, 210, 216, 35, 77,
43
+ 85, 159, 94, 215, 200, 126, 85, 42, 222, 11, 209,
44
44
  ]);
45
45
 
46
46
  // SPKI DER header for Ed25519 (RFC 8410). 12 bytes prepended to a 32-byte raw
@@ -50,14 +50,14 @@ const ED25519_SPKI_HEADER = Buffer.from([
50
50
  0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
51
51
  ]);
52
52
 
53
- const SKIP_ENV = 'FALLOW_SKIP_BINARY_VERIFY';
53
+ const SKIP_ENV = "FALLOW_SKIP_BINARY_VERIFY";
54
54
 
55
55
  function buildPublicKey(rawPubKey) {
56
56
  if (!Buffer.isBuffer(rawPubKey) || rawPubKey.length !== 32) {
57
- throw new Error('expected 32-byte raw Ed25519 public key');
57
+ throw new Error("expected 32-byte raw Ed25519 public key");
58
58
  }
59
59
  const spki = Buffer.concat([ED25519_SPKI_HEADER, rawPubKey]);
60
- return crypto.createPublicKey({ key: spki, format: 'der', type: 'spki' });
60
+ return crypto.createPublicKey({ key: spki, format: "der", type: "spki" });
61
61
  }
62
62
 
63
63
  function _verifyWithKey(binaryPath, rawPubKey) {
@@ -65,10 +65,14 @@ function _verifyWithKey(binaryPath, rawPubKey) {
65
65
  try {
66
66
  binaryBytes = fs.readFileSync(binaryPath);
67
67
  } catch (err) {
68
- if (err && err.code === 'ENOENT') {
69
- return { ok: false, code: 'binary-missing', message: `binary not found at ${binaryPath}` };
68
+ if (err && err.code === "ENOENT") {
69
+ return { ok: false, code: "binary-missing", message: `binary not found at ${binaryPath}` };
70
70
  }
71
- return { ok: false, code: 'read-error', message: `cannot read binary at ${binaryPath}: ${err.message}` };
71
+ return {
72
+ ok: false,
73
+ code: "read-error",
74
+ message: `cannot read binary at ${binaryPath}: ${err.message}`,
75
+ };
72
76
  }
73
77
 
74
78
  const sigPath = `${binaryPath}.sig`;
@@ -76,31 +80,47 @@ function _verifyWithKey(binaryPath, rawPubKey) {
76
80
  try {
77
81
  signature = fs.readFileSync(sigPath);
78
82
  } catch (err) {
79
- if (err && err.code === 'ENOENT') {
80
- return { ok: false, code: 'sig-missing', message: `signature not found at ${sigPath}` };
83
+ if (err && err.code === "ENOENT") {
84
+ return { ok: false, code: "sig-missing", message: `signature not found at ${sigPath}` };
81
85
  }
82
- return { ok: false, code: 'read-error', message: `cannot read signature at ${sigPath}: ${err.message}` };
86
+ return {
87
+ ok: false,
88
+ code: "read-error",
89
+ message: `cannot read signature at ${sigPath}: ${err.message}`,
90
+ };
83
91
  }
84
92
 
85
93
  if (signature.length !== 64) {
86
- return { ok: false, code: 'sig-invalid', message: `signature at ${sigPath} has unexpected length ${signature.length} (want 64)` };
94
+ return {
95
+ ok: false,
96
+ code: "sig-invalid",
97
+ message: `signature at ${sigPath} has unexpected length ${signature.length} (want 64)`,
98
+ };
87
99
  }
88
100
 
89
101
  let publicKey;
90
102
  try {
91
103
  publicKey = buildPublicKey(rawPubKey);
92
104
  } catch (err) {
93
- return { ok: false, code: 'key-invalid', message: `cannot construct public key: ${err.message}` };
105
+ return {
106
+ ok: false,
107
+ code: "key-invalid",
108
+ message: `cannot construct public key: ${err.message}`,
109
+ };
94
110
  }
95
111
 
96
112
  let valid;
97
113
  try {
98
114
  valid = crypto.verify(null, binaryBytes, publicKey, signature);
99
115
  } catch (err) {
100
- return { ok: false, code: 'sig-invalid', message: `crypto.verify threw: ${err.message}` };
116
+ return { ok: false, code: "sig-invalid", message: `crypto.verify threw: ${err.message}` };
101
117
  }
102
118
  if (!valid) {
103
- return { ok: false, code: 'sig-invalid', message: `Ed25519 verification failed for ${binaryPath}` };
119
+ return {
120
+ ok: false,
121
+ code: "sig-invalid",
122
+ message: `Ed25519 verification failed for ${binaryPath}`,
123
+ };
104
124
  }
105
125
  return { ok: true };
106
126
  }
@@ -110,29 +130,40 @@ function verifyBinaryAt(binaryPath) {
110
130
  }
111
131
 
112
132
  function normalizeDigest(digest) {
113
- if (typeof digest !== 'string') {
133
+ if (typeof digest !== "string") {
114
134
  return null;
115
135
  }
116
136
  const lower = digest.trim().toLowerCase();
117
- const value = lower.startsWith('sha256:') ? lower.slice('sha256:'.length) : lower;
137
+ const value = lower.startsWith("sha256:") ? lower.slice("sha256:".length) : lower;
118
138
  return /^[0-9a-f]{64}$/.test(value) ? value : null;
119
139
  }
120
140
 
121
141
  function sha256Hex(binaryPath) {
122
142
  try {
123
- return { ok: true, digest: crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex') };
143
+ return {
144
+ ok: true,
145
+ digest: crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex"),
146
+ };
124
147
  } catch (err) {
125
- if (err && err.code === 'ENOENT') {
126
- return { ok: false, code: 'binary-missing', message: `binary not found at ${binaryPath}` };
148
+ if (err && err.code === "ENOENT") {
149
+ return { ok: false, code: "binary-missing", message: `binary not found at ${binaryPath}` };
127
150
  }
128
- return { ok: false, code: 'read-error', message: `cannot read binary at ${binaryPath}: ${err.message}` };
151
+ return {
152
+ ok: false,
153
+ code: "read-error",
154
+ message: `cannot read binary at ${binaryPath}: ${err.message}`,
155
+ };
129
156
  }
130
157
  }
131
158
 
132
159
  function verifyDigestAt(binaryPath, expectedDigest) {
133
160
  const normalized = normalizeDigest(expectedDigest);
134
161
  if (!normalized) {
135
- return { ok: false, code: 'digest-invalid', message: `invalid SHA-256 digest '${expectedDigest}'` };
162
+ return {
163
+ ok: false,
164
+ code: "digest-invalid",
165
+ message: `invalid SHA-256 digest '${expectedDigest}'`,
166
+ };
136
167
  }
137
168
 
138
169
  const actual = sha256Hex(binaryPath);
@@ -142,7 +173,7 @@ function verifyDigestAt(binaryPath, expectedDigest) {
142
173
  if (actual.digest !== normalized) {
143
174
  return {
144
175
  ok: false,
145
- code: 'digest-mismatch',
176
+ code: "digest-mismatch",
146
177
  message: `SHA-256 digest mismatch for ${binaryPath}: got ${actual.digest}, want ${normalized}`,
147
178
  };
148
179
  }
@@ -153,7 +184,7 @@ function httpsJson(url, redirects = 0) {
153
184
  return new Promise((resolve, reject) => {
154
185
  const request = https.get(
155
186
  url,
156
- { headers: { 'User-Agent': 'fallow-binary-verifier' }, timeout: DIGEST_TIMEOUT_MS },
187
+ { headers: { "User-Agent": "fallow-binary-verifier" }, timeout: DIGEST_TIMEOUT_MS },
157
188
  (response) => {
158
189
  if (
159
190
  response.statusCode &&
@@ -168,11 +199,15 @@ function httpsJson(url, redirects = 0) {
168
199
  }
169
200
 
170
201
  const chunks = [];
171
- response.on('data', (chunk) => chunks.push(chunk));
172
- response.on('end', () => {
173
- const body = Buffer.concat(chunks).toString('utf8');
202
+ response.on("data", (chunk) => chunks.push(chunk));
203
+ response.on("end", () => {
204
+ const body = Buffer.concat(chunks).toString("utf8");
174
205
  if (!response.statusCode || response.statusCode >= 400) {
175
- reject(new Error(`GitHub release API returned HTTP ${response.statusCode || 'unknown'}: ${body.slice(0, 200)}`));
206
+ reject(
207
+ new Error(
208
+ `GitHub release API returned HTTP ${response.statusCode || "unknown"}: ${body.slice(0, 200)}`,
209
+ ),
210
+ );
176
211
  return;
177
212
  }
178
213
  try {
@@ -183,8 +218,10 @@ function httpsJson(url, redirects = 0) {
183
218
  });
184
219
  },
185
220
  );
186
- request.on('timeout', () => request.destroy(new Error(`timed out after ${DIGEST_TIMEOUT_MS}ms`)));
187
- request.on('error', reject);
221
+ request.on("timeout", () =>
222
+ request.destroy(new Error(`timed out after ${DIGEST_TIMEOUT_MS}ms`)),
223
+ );
224
+ request.on("error", reject);
188
225
  });
189
226
  }
190
227
 
@@ -226,28 +263,32 @@ function platformPackageDir(pkg, resolveFrom) {
226
263
  // when the manifest does not exist, cannot be parsed, lacks the field, or
227
264
  // the value is malformed. Refs #597.
228
265
  function readEmbeddedDigest(manifestPath, binaryFileName) {
229
- if (typeof manifestPath !== 'string' || manifestPath.length === 0) {
266
+ if (typeof manifestPath !== "string" || manifestPath.length === 0) {
230
267
  return null;
231
268
  }
232
269
  let manifest;
233
270
  try {
234
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
271
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
235
272
  } catch {
236
273
  return null;
237
274
  }
238
- if (!manifest || typeof manifest !== 'object') {
275
+ if (!manifest || typeof manifest !== "object") {
239
276
  return null;
240
277
  }
241
278
  const digests = manifest.fallowDigests;
242
- if (!digests || typeof digests !== 'object') {
279
+ if (!digests || typeof digests !== "object") {
243
280
  return null;
244
281
  }
245
282
  return normalizeDigest(digests[binaryFileName]);
246
283
  }
247
284
 
248
285
  function binaryTargetsForPlatform(platformId) {
249
- const isWindows = process.platform === 'win32';
250
- const ext = isWindows ? '.exe' : '';
286
+ // Derive the `.exe` suffix from the platformId, not from the live
287
+ // process.platform. Production callers pass `<platform>-<arch>...` strings
288
+ // that already encode windows-ness (`win32-x64-msvc`, `win32-arm64-msvc`),
289
+ // and tests can synthesize a Windows verify without running on Windows.
290
+ const isWindows = typeof platformId === "string" && platformId.startsWith("win32");
291
+ const ext = isWindows ? ".exe" : "";
251
292
  return [
252
293
  { binary: `fallow${ext}`, asset: `fallow-${platformId}${ext}` },
253
294
  { binary: `fallow-lsp${ext}`, asset: `fallow-lsp-${platformId}${ext}` },
@@ -257,7 +298,144 @@ function binaryTargetsForPlatform(platformId) {
257
298
 
258
299
  function isSkipRequested() {
259
300
  const v = process.env[SKIP_ENV];
260
- return v === '1' || v === 'true' || v === 'yes';
301
+ return v === "1" || v === "true" || v === "yes";
302
+ }
303
+
304
+ function currentPlatformPackageName() {
305
+ if (process.platform !== "linux") {
306
+ return getPlatformPackage(process.platform, process.arch);
307
+ }
308
+ let libcFamily;
309
+ try {
310
+ libcFamily = require("detect-libc").familySync();
311
+ } catch {
312
+ libcFamily = undefined;
313
+ }
314
+ return getPlatformPackage(process.platform, process.arch, libcFamily);
315
+ }
316
+
317
+ function readManifestForPackage(manifestPath, pkg) {
318
+ let version;
319
+ try {
320
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
321
+ version = manifest.version;
322
+ } catch (err) {
323
+ return {
324
+ ok: false,
325
+ code: "manifest-invalid",
326
+ message: `cannot read platform package manifest for ${pkg}: ${err.message}`,
327
+ package: pkg,
328
+ };
329
+ }
330
+ if (typeof version !== "string" || !version.trim()) {
331
+ return {
332
+ ok: false,
333
+ code: "manifest-invalid",
334
+ message: `platform package ${pkg} does not declare a version`,
335
+ package: pkg,
336
+ };
337
+ }
338
+ return { ok: true, version };
339
+ }
340
+
341
+ // Shared platform-package resolution used by both the async and sync verify
342
+ // paths. Returns either { ok: true, dir, manifestPath, pkg, version, platformId }
343
+ // or { ok: false, code, message, package? } matching verifyInstalled's error
344
+ // shape. dirOverride is a test-only knob; production callers must not set it.
345
+ function resolvePlatformPackageForVerify(opts) {
346
+ if (typeof opts.dirOverride === "string" && opts.dirOverride.length > 0) {
347
+ return {
348
+ ok: true,
349
+ dir: opts.dirOverride,
350
+ manifestPath: path.join(opts.dirOverride, "package.json"),
351
+ pkg: "<override>",
352
+ version: opts.version || "0.0.0",
353
+ platformId: opts.platformId || "test-platform",
354
+ };
355
+ }
356
+
357
+ const pkg = currentPlatformPackageName();
358
+ if (!pkg) {
359
+ return {
360
+ ok: false,
361
+ code: "platform-unsupported",
362
+ message: `no prebuilt binary for ${process.platform}-${process.arch}`,
363
+ };
364
+ }
365
+
366
+ let dir;
367
+ let manifestPath;
368
+ try {
369
+ ({ dir, manifestPath } = platformPackageDir(pkg, opts.resolveFrom));
370
+ } catch (err) {
371
+ return {
372
+ ok: false,
373
+ code: "platform-package-missing",
374
+ message: `platform package ${pkg} not installed: ${err.message}`,
375
+ package: pkg,
376
+ };
377
+ }
378
+
379
+ const manifest = readManifestForPackage(manifestPath, pkg);
380
+ if (!manifest.ok) return manifest;
381
+
382
+ return {
383
+ ok: true,
384
+ dir,
385
+ manifestPath,
386
+ pkg,
387
+ version: manifest.version,
388
+ platformId: pkg.replace(/^@fallow-cli\//, ""),
389
+ };
390
+ }
391
+
392
+ // Verify one binary against its sig + expected SHA-256. Used by both the
393
+ // sync and async verify-installed entry points; the digest provider may be
394
+ // sync (returns string) or async (returns Promise<string>), and the loop body
395
+ // awaits the value regardless. Keeps the outer functions a flat for-loop so
396
+ // cyclomatic + cognitive complexity stays low.
397
+ async function verifyOneBinary(target, dir, pkg, manifestPath, verifyFn, digestProvider) {
398
+ const binaryPath = path.join(dir, target.binary);
399
+ const sigResult = verifyFn(binaryPath);
400
+ if (!sigResult.ok) {
401
+ return { ...sigResult, binary: binaryPath, package: pkg };
402
+ }
403
+ // Prefer the digest embedded in the platform package's package.json
404
+ // (written at release time by `npm-prep`). Falling back to the GitHub
405
+ // release API only when no embedded digest is present preserves
406
+ // backwards compatibility with platform packages published before #597.
407
+ let expectedDigest = manifestPath ? readEmbeddedDigest(manifestPath, target.binary) : null;
408
+ if (!expectedDigest && digestProvider) {
409
+ try {
410
+ expectedDigest = await digestProvider({
411
+ assetName: target.asset,
412
+ binaryPath,
413
+ packageName: pkg,
414
+ });
415
+ } catch (err) {
416
+ return {
417
+ ok: false,
418
+ code: "digest-unavailable",
419
+ message: `cannot load SHA-256 digest for ${target.asset}: ${err.message}`,
420
+ binary: binaryPath,
421
+ package: pkg,
422
+ };
423
+ }
424
+ }
425
+ if (!expectedDigest) {
426
+ return {
427
+ ok: false,
428
+ code: "digest-unavailable",
429
+ message: "no digest",
430
+ binary: binaryPath,
431
+ package: pkg,
432
+ };
433
+ }
434
+ const digestResult = verifyDigestAt(binaryPath, expectedDigest);
435
+ if (!digestResult.ok) {
436
+ return { ...digestResult, binary: binaryPath, package: pkg };
437
+ }
438
+ return { ok: true };
261
439
  }
262
440
 
263
441
  // Locate the platform package the wrapper would use at runtime and verify
@@ -284,107 +462,118 @@ async function verifyInstalled(options) {
284
462
  return { ok: true, skipped: true, reason: `${SKIP_ENV} is set` };
285
463
  }
286
464
 
287
- const verify = typeof opts.verifyFn === 'function' ? opts.verifyFn : verifyBinaryAt;
288
- const digestProvider = typeof opts.digestProvider === 'function'
289
- ? opts.digestProvider
290
- : ({ assetName, version }) => fetchReleaseDigest(version, assetName);
465
+ const verify = typeof opts.verifyFn === "function" ? opts.verifyFn : verifyBinaryAt;
466
+ const digestProvider =
467
+ typeof opts.digestProvider === "function"
468
+ ? opts.digestProvider
469
+ : ({ assetName, version }) => fetchReleaseDigest(version, assetName);
291
470
 
292
- let dir;
293
- let manifestPath;
294
- let pkg;
295
- let version;
296
- let platformId;
297
- if (typeof opts.dirOverride === 'string' && opts.dirOverride.length > 0) {
298
- dir = opts.dirOverride;
299
- pkg = '<override>';
300
- version = opts.version || '0.0.0';
301
- platformId = opts.platformId || 'test-platform';
302
- // Lets tests drop a package.json next to the binaries to exercise the
303
- // embedded-digest path. readEmbeddedDigest returns null when missing.
304
- manifestPath = path.join(dir, 'package.json');
305
- } else {
306
- if (process.platform !== 'linux') {
307
- pkg = getPlatformPackage(process.platform, process.arch);
308
- } else {
309
- let libcFamily;
310
- try {
311
- libcFamily = require('detect-libc').familySync();
312
- } catch {
313
- // detect-libc is a dependency, but tolerate its absence the same way
314
- // postinstall.js already does.
315
- libcFamily = undefined;
316
- }
317
- pkg = getPlatformPackage(process.platform, process.arch, libcFamily);
318
- }
471
+ const resolved = resolvePlatformPackageForVerify(opts);
472
+ if (!resolved.ok) return resolved;
473
+ const { dir, manifestPath, pkg, version, platformId } = resolved;
319
474
 
320
- if (!pkg) {
321
- return { ok: false, code: 'platform-unsupported', message: `no prebuilt binary for ${process.platform}-${process.arch}` };
322
- }
475
+ // digestProvider here is always defined (the fallback above guarantees it),
476
+ // so verifyOneBinary always reaches a digest. Bind `version` into the
477
+ // provider so the per-binary loop body does not need to thread it.
478
+ const boundProvider = async (args) => digestProvider({ ...args, version });
323
479
 
324
- try {
325
- ({ dir, manifestPath } = platformPackageDir(pkg, opts.resolveFrom));
326
- } catch (err) {
327
- return { ok: false, code: 'platform-package-missing', message: `platform package ${pkg} not installed: ${err.message}`, package: pkg };
328
- }
480
+ for (const target of binaryTargetsForPlatform(platformId)) {
481
+ const result = await verifyOneBinary(target, dir, pkg, manifestPath, verify, boundProvider);
482
+ if (!result.ok) return result;
483
+ }
484
+ return { ok: true, package: pkg, version };
485
+ }
329
486
 
330
- platformId = pkg.replace(/^@fallow-cli\//, '');
331
- try {
332
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
333
- version = manifest.version;
334
- } catch (err) {
335
- return { ok: false, code: 'manifest-invalid', message: `cannot read platform package manifest for ${pkg}: ${err.message}`, package: pkg };
336
- }
337
- if (typeof version !== 'string' || !version.trim()) {
338
- return { ok: false, code: 'manifest-invalid', message: `platform package ${pkg} does not declare a version`, package: pkg };
339
- }
487
+ // Synchronous variant used by the lazy-verify first-run path in bin/fallow,
488
+ // bin/fallow-lsp, and bin/fallow-mcp. Matches verifyInstalled's result shape
489
+ // but never falls back to the GitHub Release API: callers that need network
490
+ // fallback must use the async verifyInstalled instead. This keeps bin-wrapper
491
+ // startup synchronous and bounded.
492
+ //
493
+ // options:
494
+ // allowSkipEnv - if false, ignore FALLOW_SKIP_BINARY_VERIFY. Default true.
495
+ // dirOverride - test-only directory containing binaries.
496
+ // verifyFn - replaces verifyBinaryAt for tests.
497
+ // digestProvider - sync function ({ assetName, binaryPath, packageName, version })
498
+ // returning a sha256 digest string or null. When absent and
499
+ // no embedded digest is present, returns a
500
+ // `digest-unavailable` error pointing the user at
501
+ // `npm install fallow@latest` (the embedded-digest field
502
+ // landed in 2.78.1 / #597; pre-#597 platform packages
503
+ // cannot be lazily verified). When supplied (tests), used
504
+ // in place of the embedded-digest read.
505
+ // resolveFrom - module resolution base for locating platform packages.
506
+ function verifyInstalledSync(options) {
507
+ const opts = options || {};
508
+ const skipEnvAllowed = opts.allowSkipEnv !== false;
509
+ if (skipEnvAllowed && isSkipRequested()) {
510
+ return { ok: true, skipped: true, reason: `${SKIP_ENV} is set` };
340
511
  }
341
512
 
513
+ const verify = typeof opts.verifyFn === "function" ? opts.verifyFn : verifyBinaryAt;
514
+ const digestProvider = typeof opts.digestProvider === "function" ? opts.digestProvider : null;
515
+
516
+ const resolved = resolvePlatformPackageForVerify(opts);
517
+ if (!resolved.ok) return resolved;
518
+ const { dir, manifestPath, pkg, version, platformId } = resolved;
519
+
342
520
  for (const target of binaryTargetsForPlatform(platformId)) {
343
- const binaryPath = path.join(dir, target.binary);
344
- const result = verify(binaryPath);
345
- if (!result.ok) {
346
- return { ...result, binary: binaryPath, package: pkg };
347
- }
348
- // Prefer the digest embedded in the platform package's package.json
349
- // (written at release time by `npm-prep`). Falling back to the GitHub
350
- // release API only when no embedded digest is present preserves
351
- // backwards compatibility with platform packages published before #597.
352
- // The shared-IP CI rate-limit failure mode that motivated #597 only
353
- // affects the fallback path, so the steady-state install path is now
354
- // fully offline.
355
- let expectedDigest = manifestPath
356
- ? readEmbeddedDigest(manifestPath, target.binary)
357
- : null;
358
- if (!expectedDigest) {
359
- try {
360
- expectedDigest = await digestProvider({
361
- assetName: target.asset,
362
- binaryPath,
363
- packageName: pkg,
364
- version,
365
- });
366
- } catch (err) {
367
- return {
368
- ok: false,
369
- code: 'digest-unavailable',
370
- message: `cannot load SHA-256 digest for ${target.asset}: ${err.message}`,
371
- binary: binaryPath,
372
- package: pkg,
373
- };
374
- }
375
- }
376
- const digestResult = verifyDigestAt(binaryPath, expectedDigest);
377
- if (!digestResult.ok) {
378
- return { ...digestResult, binary: binaryPath, package: pkg };
379
- }
521
+ const result = verifyOneBinarySync(target, dir, pkg, manifestPath, verify, digestProvider);
522
+ if (!result.ok) return result;
380
523
  }
381
524
  return { ok: true, package: pkg, version };
382
525
  }
383
526
 
527
+ // Sync counterpart to verifyOneBinary. Different from the async version in
528
+ // three ways: no `await`, the missing-embedded-digest path returns a clear
529
+ // actionable error pointing the user at `npm install fallow@latest` (since
530
+ // there is no network fallback in lazy mode), and the digestProvider is
531
+ // optional (tests inject one; production callers rely on the embedded digest).
532
+ function verifyOneBinarySync(target, dir, pkg, manifestPath, verifyFn, digestProvider) {
533
+ const binaryPath = path.join(dir, target.binary);
534
+ const sigResult = verifyFn(binaryPath);
535
+ if (!sigResult.ok) {
536
+ return { ...sigResult, binary: binaryPath, package: pkg };
537
+ }
538
+ let expectedDigest = manifestPath ? readEmbeddedDigest(manifestPath, target.binary) : null;
539
+ if (!expectedDigest && digestProvider) {
540
+ try {
541
+ expectedDigest = digestProvider({ assetName: target.asset, binaryPath, packageName: pkg });
542
+ } catch (err) {
543
+ return {
544
+ ok: false,
545
+ code: "digest-unavailable",
546
+ message: `cannot load SHA-256 digest for ${target.asset}: ${err.message}`,
547
+ binary: binaryPath,
548
+ package: pkg,
549
+ };
550
+ }
551
+ }
552
+ if (!expectedDigest) {
553
+ return {
554
+ ok: false,
555
+ code: "digest-unavailable",
556
+ message:
557
+ `no embedded SHA-256 digest for ${target.binary} in ${pkg} ` +
558
+ `(platform package predates fallow 2.78.1). ` +
559
+ `Run \`npm install fallow@latest\` to refresh, or set ${SKIP_ENV}=1 ` +
560
+ `to bypass verification (logged once per process).`,
561
+ binary: binaryPath,
562
+ package: pkg,
563
+ };
564
+ }
565
+ const digestResult = verifyDigestAt(binaryPath, expectedDigest);
566
+ if (!digestResult.ok) {
567
+ return { ...digestResult, binary: binaryPath, package: pkg };
568
+ }
569
+ return { ok: true };
570
+ }
571
+
384
572
  module.exports = {
385
573
  verifyBinaryAt,
386
574
  verifyDigestAt,
387
575
  verifyInstalled,
576
+ verifyInstalledSync,
388
577
  _verifyWithKey,
389
578
  normalizeDigest,
390
579
  readEmbeddedDigest,