fallow 2.80.0 → 2.82.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.
@@ -1,45 +1,46 @@
1
- const test = require('node:test');
2
- const assert = require('node:assert/strict');
3
- const crypto = require('node:crypto');
4
- const fs = require('node:fs');
5
- const os = require('node:os');
6
- const path = require('node:path');
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const crypto = require("node:crypto");
4
+ const fs = require("node:fs");
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
7
 
8
8
  const {
9
9
  _verifyWithKey,
10
10
  verifyBinaryAt,
11
11
  verifyDigestAt,
12
12
  verifyInstalled,
13
+ verifyInstalledSync,
13
14
  sha256Hex,
14
15
  normalizeDigest,
15
16
  readEmbeddedDigest,
16
17
  EMBEDDED_PUBLIC_KEY,
17
18
  ED25519_SPKI_HEADER,
18
19
  SKIP_ENV,
19
- } = require('./verify-binary');
20
- const { getPlatformPackage } = require('./platform-package');
20
+ } = require("./verify-binary");
21
+ const { getPlatformPackage } = require("./platform-package");
21
22
 
22
- function makeDigestProvider(dir) {
23
- return ({ assetName, binaryPath }) => {
23
+ function makeDigestProvider(_dir) {
24
+ return ({ binaryPath }) => {
24
25
  const data = fs.readFileSync(binaryPath);
25
- return Promise.resolve('sha256:' + crypto.createHash('sha256').update(data).digest('hex'));
26
+ return Promise.resolve("sha256:" + crypto.createHash("sha256").update(data).digest("hex"));
26
27
  };
27
28
  }
28
29
 
29
30
  function makeMismatchedDigestProvider() {
30
- return () => Promise.resolve('sha256:' + 'a'.repeat(64));
31
+ return () => Promise.resolve("sha256:" + "a".repeat(64));
31
32
  }
32
33
 
33
34
  function makeKeypair() {
34
- const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
35
- const spki = publicKey.export({ format: 'der', type: 'spki' });
35
+ const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
36
+ const spki = publicKey.export({ format: "der", type: "spki" });
36
37
  const rawPub = spki.subarray(spki.length - 32);
37
38
  return { privateKey, rawPub };
38
39
  }
39
40
 
40
41
  function makeFixture(binaryBytes, signFn) {
41
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-'));
42
- const binaryPath = path.join(dir, 'fallow');
42
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
43
+ const binaryPath = path.join(dir, "fallow");
43
44
  fs.writeFileSync(binaryPath, binaryBytes);
44
45
  if (signFn) {
45
46
  fs.writeFileSync(`${binaryPath}.sig`, signFn(binaryBytes));
@@ -51,23 +52,21 @@ function cleanup(dir) {
51
52
  fs.rmSync(dir, { recursive: true, force: true });
52
53
  }
53
54
 
54
- test('embedded public key is 32 bytes and SPKI header is 12 bytes', () => {
55
+ test("embedded public key is 32 bytes and SPKI header is 12 bytes", () => {
55
56
  assert.equal(EMBEDDED_PUBLIC_KEY.length, 32);
56
57
  assert.equal(ED25519_SPKI_HEADER.length, 12);
57
58
  });
58
59
 
59
- test('embedded public key reconstructs a valid Ed25519 SPKI key', () => {
60
+ test("embedded public key reconstructs a valid Ed25519 SPKI key", () => {
60
61
  const spki = Buffer.concat([ED25519_SPKI_HEADER, EMBEDDED_PUBLIC_KEY]);
61
- const key = crypto.createPublicKey({ key: spki, format: 'der', type: 'spki' });
62
- assert.equal(key.asymmetricKeyType, 'ed25519');
62
+ const key = crypto.createPublicKey({ key: spki, format: "der", type: "spki" });
63
+ assert.equal(key.asymmetricKeyType, "ed25519");
63
64
  });
64
65
 
65
- test('_verifyWithKey returns ok for a valid signature', () => {
66
+ test("_verifyWithKey returns ok for a valid signature", () => {
66
67
  const { privateKey, rawPub } = makeKeypair();
67
- const content = Buffer.from('hello world');
68
- const { dir, binaryPath } = makeFixture(content, (data) =>
69
- crypto.sign(null, data, privateKey),
70
- );
68
+ const content = Buffer.from("hello world");
69
+ const { dir, binaryPath } = makeFixture(content, (data) => crypto.sign(null, data, privateKey));
71
70
  try {
72
71
  const result = _verifyWithKey(binaryPath, rawPub);
73
72
  assert.deepEqual(result, { ok: true });
@@ -76,119 +75,113 @@ test('_verifyWithKey returns ok for a valid signature', () => {
76
75
  }
77
76
  });
78
77
 
79
- test('_verifyWithKey returns sig-invalid when the signature is corrupted', () => {
78
+ test("_verifyWithKey returns sig-invalid when the signature is corrupted", () => {
80
79
  const { privateKey, rawPub } = makeKeypair();
81
- const content = Buffer.from('hello world');
82
- const { dir, binaryPath } = makeFixture(content, (data) =>
83
- crypto.sign(null, data, privateKey),
84
- );
80
+ const content = Buffer.from("hello world");
81
+ const { dir, binaryPath } = makeFixture(content, (data) => crypto.sign(null, data, privateKey));
85
82
  try {
86
83
  const sig = fs.readFileSync(`${binaryPath}.sig`);
87
84
  sig[0] ^= 0xff;
88
85
  fs.writeFileSync(`${binaryPath}.sig`, sig);
89
86
  const result = _verifyWithKey(binaryPath, rawPub);
90
87
  assert.equal(result.ok, false);
91
- assert.equal(result.code, 'sig-invalid');
88
+ assert.equal(result.code, "sig-invalid");
92
89
  } finally {
93
90
  cleanup(dir);
94
91
  }
95
92
  });
96
93
 
97
- test('_verifyWithKey returns sig-invalid for the wrong key', () => {
94
+ test("_verifyWithKey returns sig-invalid for the wrong key", () => {
98
95
  const { rawPub } = makeKeypair();
99
96
  const wrongKey = makeKeypair();
100
- const content = Buffer.from('hello world');
97
+ const content = Buffer.from("hello world");
101
98
  const { dir, binaryPath } = makeFixture(content, (data) =>
102
99
  crypto.sign(null, data, wrongKey.privateKey),
103
100
  );
104
101
  try {
105
102
  const result = _verifyWithKey(binaryPath, rawPub);
106
103
  assert.equal(result.ok, false);
107
- assert.equal(result.code, 'sig-invalid');
104
+ assert.equal(result.code, "sig-invalid");
108
105
  } finally {
109
106
  cleanup(dir);
110
107
  }
111
108
  });
112
109
 
113
- test('_verifyWithKey returns sig-invalid when the binary bytes are tampered', () => {
110
+ test("_verifyWithKey returns sig-invalid when the binary bytes are tampered", () => {
114
111
  const { privateKey, rawPub } = makeKeypair();
115
- const original = Buffer.from('hello world');
116
- const { dir, binaryPath } = makeFixture(original, (data) =>
117
- crypto.sign(null, data, privateKey),
118
- );
112
+ const original = Buffer.from("hello world");
113
+ const { dir, binaryPath } = makeFixture(original, (data) => crypto.sign(null, data, privateKey));
119
114
  try {
120
- fs.writeFileSync(binaryPath, Buffer.from('tampered'));
115
+ fs.writeFileSync(binaryPath, Buffer.from("tampered"));
121
116
  const result = _verifyWithKey(binaryPath, rawPub);
122
117
  assert.equal(result.ok, false);
123
- assert.equal(result.code, 'sig-invalid');
118
+ assert.equal(result.code, "sig-invalid");
124
119
  } finally {
125
120
  cleanup(dir);
126
121
  }
127
122
  });
128
123
 
129
- test('_verifyWithKey returns sig-missing when the signature file does not exist', () => {
124
+ test("_verifyWithKey returns sig-missing when the signature file does not exist", () => {
130
125
  const { rawPub } = makeKeypair();
131
- const { dir, binaryPath } = makeFixture(Buffer.from('hello world'));
126
+ const { dir, binaryPath } = makeFixture(Buffer.from("hello world"));
132
127
  try {
133
128
  const result = _verifyWithKey(binaryPath, rawPub);
134
129
  assert.equal(result.ok, false);
135
- assert.equal(result.code, 'sig-missing');
130
+ assert.equal(result.code, "sig-missing");
136
131
  } finally {
137
132
  cleanup(dir);
138
133
  }
139
134
  });
140
135
 
141
- test('_verifyWithKey returns binary-missing when the binary does not exist', () => {
136
+ test("_verifyWithKey returns binary-missing when the binary does not exist", () => {
142
137
  const { rawPub } = makeKeypair();
143
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-'));
138
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
144
139
  try {
145
- const result = _verifyWithKey(path.join(dir, 'nonexistent'), rawPub);
140
+ const result = _verifyWithKey(path.join(dir, "nonexistent"), rawPub);
146
141
  assert.equal(result.ok, false);
147
- assert.equal(result.code, 'binary-missing');
142
+ assert.equal(result.code, "binary-missing");
148
143
  } finally {
149
144
  cleanup(dir);
150
145
  }
151
146
  });
152
147
 
153
- test('_verifyWithKey returns sig-invalid when the signature length is wrong', () => {
148
+ test("_verifyWithKey returns sig-invalid when the signature length is wrong", () => {
154
149
  const { rawPub } = makeKeypair();
155
- const { dir, binaryPath } = makeFixture(Buffer.from('hello'));
150
+ const { dir, binaryPath } = makeFixture(Buffer.from("hello"));
156
151
  try {
157
- fs.writeFileSync(`${binaryPath}.sig`, Buffer.from('short'));
152
+ fs.writeFileSync(`${binaryPath}.sig`, Buffer.from("short"));
158
153
  const result = _verifyWithKey(binaryPath, rawPub);
159
154
  assert.equal(result.ok, false);
160
- assert.equal(result.code, 'sig-invalid');
155
+ assert.equal(result.code, "sig-invalid");
161
156
  } finally {
162
157
  cleanup(dir);
163
158
  }
164
159
  });
165
160
 
166
- test('_verifyWithKey throws when given a non-32-byte raw public key', () => {
167
- const { dir, binaryPath } = makeFixture(Buffer.from('hello world'));
161
+ test("_verifyWithKey throws when given a non-32-byte raw public key", () => {
162
+ const { dir, binaryPath } = makeFixture(Buffer.from("hello world"));
168
163
  try {
169
- const result = _verifyWithKey(binaryPath, Buffer.from('too short'));
164
+ const result = _verifyWithKey(binaryPath, Buffer.from("too short"));
170
165
  assert.equal(result.ok, false);
171
- assert.equal(result.code, 'sig-missing');
166
+ assert.equal(result.code, "sig-missing");
172
167
  } finally {
173
168
  cleanup(dir);
174
169
  }
175
170
  });
176
171
 
177
- test('verifyBinaryAt uses the embedded production public key', () => {
172
+ test("verifyBinaryAt uses the embedded production public key", () => {
178
173
  // The embedded key cannot sign our test data because we do not have the
179
174
  // private key, so we only assert that verifyBinaryAt returns sig-invalid
180
175
  // for a random signature against the production key, not the underlying
181
176
  // crypto throwing. This locks in that the public API uses the embedded
182
177
  // key path.
183
178
  const { privateKey } = makeKeypair();
184
- const content = Buffer.from('hello world');
185
- const { dir, binaryPath } = makeFixture(content, (data) =>
186
- crypto.sign(null, data, privateKey),
187
- );
179
+ const content = Buffer.from("hello world");
180
+ const { dir, binaryPath } = makeFixture(content, (data) => crypto.sign(null, data, privateKey));
188
181
  try {
189
182
  const result = verifyBinaryAt(binaryPath);
190
183
  assert.equal(result.ok, false);
191
- assert.equal(result.code, 'sig-invalid');
184
+ assert.equal(result.code, "sig-invalid");
192
185
  } finally {
193
186
  cleanup(dir);
194
187
  }
@@ -196,16 +189,16 @@ test('verifyBinaryAt uses the embedded production public key', () => {
196
189
 
197
190
  function makePlatformDir(privateKey, options) {
198
191
  const opts = options || {};
199
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-'));
200
- const ext = process.platform === 'win32' ? '.exe' : '';
201
- for (const base of ['fallow', 'fallow-lsp', 'fallow-mcp']) {
192
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
193
+ const ext = process.platform === "win32" ? ".exe" : "";
194
+ for (const base of ["fallow", "fallow-lsp", "fallow-mcp"]) {
202
195
  const binaryPath = path.join(dir, `${base}${ext}`);
203
196
  const content = Buffer.from(`mock ${base} contents`);
204
197
  fs.writeFileSync(binaryPath, content);
205
198
  if (opts.skipSigFor === base) {
206
199
  continue;
207
200
  }
208
- const data = opts.corruptBinaryFor === base ? Buffer.from('tampered') : content;
201
+ const data = opts.corruptBinaryFor === base ? Buffer.from("tampered") : content;
209
202
  const sig = crypto.sign(null, data, privateKey);
210
203
  if (opts.corruptSigFor === base) {
211
204
  sig[0] ^= 0xff;
@@ -216,58 +209,61 @@ function makePlatformDir(privateKey, options) {
216
209
  }
217
210
 
218
211
  function currentPlatformPackage() {
219
- if (process.platform !== 'linux') {
212
+ if (process.platform !== "linux") {
220
213
  return getPlatformPackage(process.platform, process.arch);
221
214
  }
222
215
  let libcFamily;
223
216
  try {
224
- libcFamily = require('detect-libc').familySync();
217
+ libcFamily = require("detect-libc").familySync();
225
218
  } catch {
226
219
  libcFamily = undefined;
227
220
  }
228
221
  return getPlatformPackage(process.platform, process.arch, libcFamily);
229
222
  }
230
223
 
231
- test('normalizeDigest accepts sha256: prefix and bare hex', () => {
232
- const sample = 'a'.repeat(64);
233
- assert.equal(normalizeDigest('sha256:' + sample), sample);
224
+ test("normalizeDigest accepts sha256: prefix and bare hex", () => {
225
+ const sample = "a".repeat(64);
226
+ assert.equal(normalizeDigest("sha256:" + sample), sample);
234
227
  assert.equal(normalizeDigest(sample), sample);
235
- assert.equal(normalizeDigest('SHA256:' + sample.toUpperCase()), sample);
228
+ assert.equal(normalizeDigest("SHA256:" + sample.toUpperCase()), sample);
236
229
  });
237
230
 
238
- test('normalizeDigest rejects malformed digests', () => {
231
+ test("normalizeDigest rejects malformed digests", () => {
239
232
  assert.equal(normalizeDigest(null), null);
240
- assert.equal(normalizeDigest(''), null);
241
- assert.equal(normalizeDigest('not-hex'), null);
242
- assert.equal(normalizeDigest('a'.repeat(63)), null);
233
+ assert.equal(normalizeDigest(""), null);
234
+ assert.equal(normalizeDigest("not-hex"), null);
235
+ assert.equal(normalizeDigest("a".repeat(63)), null);
243
236
  });
244
237
 
245
- test('sha256Hex returns 64-char hex over file bytes', () => {
246
- const { dir, binaryPath } = makeFixture(Buffer.from('hello world'));
238
+ test("sha256Hex returns 64-char hex over file bytes", () => {
239
+ const { dir, binaryPath } = makeFixture(Buffer.from("hello world"));
247
240
  try {
248
241
  const result = sha256Hex(binaryPath);
249
242
  assert.equal(result.ok, true);
250
- assert.equal(result.digest, crypto.createHash('sha256').update(Buffer.from('hello world')).digest('hex'));
243
+ assert.equal(
244
+ result.digest,
245
+ crypto.createHash("sha256").update(Buffer.from("hello world")).digest("hex"),
246
+ );
251
247
  } finally {
252
248
  cleanup(dir);
253
249
  }
254
250
  });
255
251
 
256
- test('verifyDigestAt accepts matching digest and rejects mismatched', () => {
257
- const { dir, binaryPath } = makeFixture(Buffer.from('hello world'));
252
+ test("verifyDigestAt accepts matching digest and rejects mismatched", () => {
253
+ const { dir, binaryPath } = makeFixture(Buffer.from("hello world"));
258
254
  try {
259
- const correct = crypto.createHash('sha256').update(Buffer.from('hello world')).digest('hex');
260
- assert.deepEqual(verifyDigestAt(binaryPath, 'sha256:' + correct), { ok: true });
261
- const wrong = 'b'.repeat(64);
255
+ const correct = crypto.createHash("sha256").update(Buffer.from("hello world")).digest("hex");
256
+ assert.deepEqual(verifyDigestAt(binaryPath, "sha256:" + correct), { ok: true });
257
+ const wrong = "b".repeat(64);
262
258
  const bad = verifyDigestAt(binaryPath, wrong);
263
259
  assert.equal(bad.ok, false);
264
- assert.equal(bad.code, 'digest-mismatch');
260
+ assert.equal(bad.code, "digest-mismatch");
265
261
  } finally {
266
262
  cleanup(dir);
267
263
  }
268
264
  });
269
265
 
270
- test('verifyInstalled with dirOverride returns ok when every binary verifies', async (t) => {
266
+ test("verifyInstalled with dirOverride returns ok when every binary verifies", async (t) => {
271
267
  const { privateKey, rawPub } = makeKeypair();
272
268
  const dir = makePlatformDir(privateKey);
273
269
  t.after(() => cleanup(dir));
@@ -277,28 +273,31 @@ test('verifyInstalled with dirOverride returns ok when every binary verifies', a
277
273
  digestProvider: makeDigestProvider(dir),
278
274
  });
279
275
  assert.equal(result.ok, true);
280
- assert.equal(result.package, '<override>');
276
+ assert.equal(result.package, "<override>");
281
277
  });
282
278
 
283
- test('verifyInstalled resolves a global npm install from the fallow package directory', async (t) => {
279
+ test("verifyInstalled resolves a global npm install from the fallow package directory", async (t) => {
284
280
  const pkg = currentPlatformPackage();
285
281
  if (!pkg) {
286
- t.skip('unsupported platform');
282
+ t.skip("unsupported platform");
287
283
  return;
288
284
  }
289
285
 
290
286
  const { privateKey, rawPub } = makeKeypair();
291
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-global-'));
287
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-global-"));
292
288
  t.after(() => cleanup(root));
293
289
 
294
- const resolveFrom = path.join(root, 'node_modules', 'fallow');
295
- const platformDir = path.join(root, 'node_modules', ...pkg.split('/'));
290
+ const resolveFrom = path.join(root, "node_modules", "fallow");
291
+ const platformDir = path.join(root, "node_modules", ...pkg.split("/"));
296
292
  fs.mkdirSync(resolveFrom, { recursive: true });
297
293
  fs.mkdirSync(platformDir, { recursive: true });
298
- fs.writeFileSync(path.join(platformDir, 'package.json'), JSON.stringify({ name: pkg, version: '9.9.9' }));
294
+ fs.writeFileSync(
295
+ path.join(platformDir, "package.json"),
296
+ JSON.stringify({ name: pkg, version: "9.9.9" }),
297
+ );
299
298
 
300
- const ext = process.platform === 'win32' ? '.exe' : '';
301
- for (const base of ['fallow', 'fallow-lsp', 'fallow-mcp']) {
299
+ const ext = process.platform === "win32" ? ".exe" : "";
300
+ for (const base of ["fallow", "fallow-lsp", "fallow-mcp"]) {
302
301
  const binaryPath = path.join(platformDir, `${base}${ext}`);
303
302
  const content = Buffer.from(`global install ${base}`);
304
303
  fs.writeFileSync(binaryPath, content);
@@ -308,16 +307,17 @@ test('verifyInstalled resolves a global npm install from the fallow package dire
308
307
  const result = await verifyInstalled({
309
308
  resolveFrom,
310
309
  verifyFn: (p) => _verifyWithKey(p, rawPub),
311
- digestProvider: ({ binaryPath }) => crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'),
310
+ digestProvider: ({ binaryPath }) =>
311
+ crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex"),
312
312
  });
313
313
  assert.equal(result.ok, true);
314
314
  assert.equal(result.package, pkg);
315
- assert.equal(result.version, '9.9.9');
315
+ assert.equal(result.version, "9.9.9");
316
316
  });
317
317
 
318
- test('verifyInstalled with dirOverride fails fast on the first bad signature', async (t) => {
318
+ test("verifyInstalled with dirOverride fails fast on the first bad signature", async (t) => {
319
319
  const { privateKey, rawPub } = makeKeypair();
320
- const dir = makePlatformDir(privateKey, { corruptSigFor: 'fallow-lsp' });
320
+ const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow-lsp" });
321
321
  t.after(() => cleanup(dir));
322
322
  const result = await verifyInstalled({
323
323
  dirOverride: dir,
@@ -325,13 +325,13 @@ test('verifyInstalled with dirOverride fails fast on the first bad signature', a
325
325
  digestProvider: makeDigestProvider(dir),
326
326
  });
327
327
  assert.equal(result.ok, false);
328
- assert.equal(result.code, 'sig-invalid');
328
+ assert.equal(result.code, "sig-invalid");
329
329
  assert.match(result.binary, /fallow-lsp/);
330
330
  });
331
331
 
332
- test('verifyInstalled with dirOverride reports sig-missing when a .sig is absent', async (t) => {
332
+ test("verifyInstalled with dirOverride reports sig-missing when a .sig is absent", async (t) => {
333
333
  const { privateKey, rawPub } = makeKeypair();
334
- const dir = makePlatformDir(privateKey, { skipSigFor: 'fallow-mcp' });
334
+ const dir = makePlatformDir(privateKey, { skipSigFor: "fallow-mcp" });
335
335
  t.after(() => cleanup(dir));
336
336
  const result = await verifyInstalled({
337
337
  dirOverride: dir,
@@ -339,11 +339,11 @@ test('verifyInstalled with dirOverride reports sig-missing when a .sig is absent
339
339
  digestProvider: makeDigestProvider(dir),
340
340
  });
341
341
  assert.equal(result.ok, false);
342
- assert.equal(result.code, 'sig-missing');
342
+ assert.equal(result.code, "sig-missing");
343
343
  assert.match(result.binary, /fallow-mcp/);
344
344
  });
345
345
 
346
- test('verifyInstalled reports digest-mismatch when SHA-256 disagrees with the provider', async (t) => {
346
+ test("verifyInstalled reports digest-mismatch when SHA-256 disagrees with the provider", async (t) => {
347
347
  const { privateKey, rawPub } = makeKeypair();
348
348
  const dir = makePlatformDir(privateKey);
349
349
  t.after(() => cleanup(dir));
@@ -353,91 +353,92 @@ test('verifyInstalled reports digest-mismatch when SHA-256 disagrees with the pr
353
353
  digestProvider: makeMismatchedDigestProvider(),
354
354
  });
355
355
  assert.equal(result.ok, false);
356
- assert.equal(result.code, 'digest-mismatch');
356
+ assert.equal(result.code, "digest-mismatch");
357
357
  });
358
358
 
359
- test('verifyInstalled reports digest-unavailable when the provider rejects', async (t) => {
359
+ test("verifyInstalled reports digest-unavailable when the provider rejects", async (t) => {
360
360
  const { privateKey, rawPub } = makeKeypair();
361
361
  const dir = makePlatformDir(privateKey);
362
362
  t.after(() => cleanup(dir));
363
363
  const result = await verifyInstalled({
364
364
  dirOverride: dir,
365
365
  verifyFn: (p) => _verifyWithKey(p, rawPub),
366
- digestProvider: () => Promise.reject(new Error('network down')),
366
+ digestProvider: () => Promise.reject(new Error("network down")),
367
367
  });
368
368
  assert.equal(result.ok, false);
369
- assert.equal(result.code, 'digest-unavailable');
369
+ assert.equal(result.code, "digest-unavailable");
370
370
  assert.match(result.message, /network down/);
371
371
  });
372
372
 
373
- test('verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY', async (t) => {
373
+ test("verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY", async (t) => {
374
374
  const previous = process.env[SKIP_ENV];
375
- process.env[SKIP_ENV] = '1';
375
+ process.env[SKIP_ENV] = "1";
376
376
  t.after(() => {
377
377
  if (previous === undefined) delete process.env[SKIP_ENV];
378
378
  else process.env[SKIP_ENV] = previous;
379
379
  });
380
- const result = await verifyInstalled({ dirOverride: '/does/not/exist' });
380
+ const result = await verifyInstalled({ dirOverride: "/does/not/exist" });
381
381
  assert.equal(result.ok, true);
382
382
  assert.equal(result.skipped, true);
383
383
  });
384
384
 
385
385
  function computeDigestsForDir(dir) {
386
- const ext = process.platform === 'win32' ? '.exe' : '';
386
+ const ext = process.platform === "win32" ? ".exe" : "";
387
387
  const out = {};
388
- for (const base of ['fallow', 'fallow-lsp', 'fallow-mcp']) {
388
+ for (const base of ["fallow", "fallow-lsp", "fallow-mcp"]) {
389
389
  const fileName = `${base}${ext}`;
390
390
  const full = path.join(dir, fileName);
391
- out[fileName] = 'sha256:' + crypto.createHash('sha256').update(fs.readFileSync(full)).digest('hex');
391
+ out[fileName] =
392
+ "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex");
392
393
  }
393
394
  return out;
394
395
  }
395
396
 
396
397
  function writeManifest(dir, body) {
397
- fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(body));
398
+ fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(body));
398
399
  }
399
400
 
400
- test('readEmbeddedDigest returns null when manifest is missing', () => {
401
- assert.equal(readEmbeddedDigest('/does/not/exist/package.json', 'fallow'), null);
401
+ test("readEmbeddedDigest returns null when manifest is missing", () => {
402
+ assert.equal(readEmbeddedDigest("/does/not/exist/package.json", "fallow"), null);
402
403
  });
403
404
 
404
- test('readEmbeddedDigest returns null when fallowDigests field is absent', (t) => {
405
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-emb-'));
405
+ test("readEmbeddedDigest returns null when fallowDigests field is absent", (t) => {
406
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-emb-"));
406
407
  t.after(() => cleanup(dir));
407
- writeManifest(dir, { name: '@fallow-cli/x', version: '1.0.0' });
408
- assert.equal(readEmbeddedDigest(path.join(dir, 'package.json'), 'fallow'), null);
408
+ writeManifest(dir, { name: "@fallow-cli/x", version: "1.0.0" });
409
+ assert.equal(readEmbeddedDigest(path.join(dir, "package.json"), "fallow"), null);
409
410
  });
410
411
 
411
- test('readEmbeddedDigest returns null when the per-binary entry is malformed', (t) => {
412
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-emb-'));
412
+ test("readEmbeddedDigest returns null when the per-binary entry is malformed", (t) => {
413
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-emb-"));
413
414
  t.after(() => cleanup(dir));
414
415
  writeManifest(dir, {
415
- name: '@fallow-cli/x',
416
- version: '1.0.0',
417
- fallowDigests: { fallow: 'not-a-real-digest' },
416
+ name: "@fallow-cli/x",
417
+ version: "1.0.0",
418
+ fallowDigests: { fallow: "not-a-real-digest" },
418
419
  });
419
- assert.equal(readEmbeddedDigest(path.join(dir, 'package.json'), 'fallow'), null);
420
+ assert.equal(readEmbeddedDigest(path.join(dir, "package.json"), "fallow"), null);
420
421
  });
421
422
 
422
- test('readEmbeddedDigest returns normalized hex for a valid sha256: prefixed entry', (t) => {
423
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-emb-'));
423
+ test("readEmbeddedDigest returns normalized hex for a valid sha256: prefixed entry", (t) => {
424
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-emb-"));
424
425
  t.after(() => cleanup(dir));
425
- const hex = 'a'.repeat(64);
426
+ const hex = "a".repeat(64);
426
427
  writeManifest(dir, {
427
- name: '@fallow-cli/x',
428
- version: '1.0.0',
429
- fallowDigests: { fallow: 'sha256:' + hex },
428
+ name: "@fallow-cli/x",
429
+ version: "1.0.0",
430
+ fallowDigests: { fallow: "sha256:" + hex },
430
431
  });
431
- assert.equal(readEmbeddedDigest(path.join(dir, 'package.json'), 'fallow'), hex);
432
+ assert.equal(readEmbeddedDigest(path.join(dir, "package.json"), "fallow"), hex);
432
433
  });
433
434
 
434
- test('verifyInstalled uses the embedded digest without calling the provider', async (t) => {
435
+ test("verifyInstalled uses the embedded digest without calling the provider", async (t) => {
435
436
  const { privateKey, rawPub } = makeKeypair();
436
437
  const dir = makePlatformDir(privateKey);
437
438
  t.after(() => cleanup(dir));
438
439
  writeManifest(dir, {
439
- name: '@fallow-cli/x',
440
- version: '1.0.0',
440
+ name: "@fallow-cli/x",
441
+ version: "1.0.0",
441
442
  fallowDigests: computeDigestsForDir(dir),
442
443
  });
443
444
  let providerCalls = 0;
@@ -446,14 +447,14 @@ test('verifyInstalled uses the embedded digest without calling the provider', as
446
447
  verifyFn: (p) => _verifyWithKey(p, rawPub),
447
448
  digestProvider: () => {
448
449
  providerCalls += 1;
449
- return Promise.reject(new Error('provider should not be called'));
450
+ return Promise.reject(new Error("provider should not be called"));
450
451
  },
451
452
  });
452
453
  assert.equal(result.ok, true, JSON.stringify(result));
453
454
  assert.equal(providerCalls, 0);
454
455
  });
455
456
 
456
- test('verifyInstalled falls back to the provider when the embedded digest is missing', async (t) => {
457
+ test("verifyInstalled falls back to the provider when the embedded digest is missing", async (t) => {
457
458
  const { privateKey, rawPub } = makeKeypair();
458
459
  const dir = makePlatformDir(privateKey);
459
460
  t.after(() => cleanup(dir));
@@ -464,21 +465,23 @@ test('verifyInstalled falls back to the provider when the embedded digest is mis
464
465
  verifyFn: (p) => _verifyWithKey(p, rawPub),
465
466
  digestProvider: ({ binaryPath }) => {
466
467
  providerCalls += 1;
467
- return Promise.resolve('sha256:' + crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'));
468
+ return Promise.resolve(
469
+ "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex"),
470
+ );
468
471
  },
469
472
  });
470
473
  assert.equal(result.ok, true);
471
474
  assert.equal(providerCalls, 3);
472
475
  });
473
476
 
474
- test('verifyInstalled falls back to the provider when fallowDigests is partial / malformed', async (t) => {
477
+ test("verifyInstalled falls back to the provider when fallowDigests is partial / malformed", async (t) => {
475
478
  const { privateKey, rawPub } = makeKeypair();
476
479
  const dir = makePlatformDir(privateKey);
477
480
  t.after(() => cleanup(dir));
478
481
  writeManifest(dir, {
479
- name: '@fallow-cli/x',
480
- version: '1.0.0',
481
- fallowDigests: { fallow: 'not-a-real-digest' },
482
+ name: "@fallow-cli/x",
483
+ version: "1.0.0",
484
+ fallowDigests: { fallow: "not-a-real-digest" },
482
485
  });
483
486
  let providerCalls = 0;
484
487
  const result = await verifyInstalled({
@@ -486,7 +489,9 @@ test('verifyInstalled falls back to the provider when fallowDigests is partial /
486
489
  verifyFn: (p) => _verifyWithKey(p, rawPub),
487
490
  digestProvider: ({ binaryPath }) => {
488
491
  providerCalls += 1;
489
- return Promise.resolve('sha256:' + crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'));
492
+ return Promise.resolve(
493
+ "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex"),
494
+ );
490
495
  },
491
496
  });
492
497
  assert.equal(result.ok, true);
@@ -494,32 +499,32 @@ test('verifyInstalled falls back to the provider when fallowDigests is partial /
494
499
  assert.equal(providerCalls, 3);
495
500
  });
496
501
 
497
- test('verifyInstalled returns digest-mismatch when the embedded digest disagrees with the binary', async (t) => {
502
+ test("verifyInstalled returns digest-mismatch when the embedded digest disagrees with the binary", async (t) => {
498
503
  const { privateKey, rawPub } = makeKeypair();
499
504
  const dir = makePlatformDir(privateKey);
500
505
  t.after(() => cleanup(dir));
501
- const ext = process.platform === 'win32' ? '.exe' : '';
506
+ const ext = process.platform === "win32" ? ".exe" : "";
502
507
  writeManifest(dir, {
503
- name: '@fallow-cli/x',
504
- version: '1.0.0',
508
+ name: "@fallow-cli/x",
509
+ version: "1.0.0",
505
510
  fallowDigests: {
506
- [`fallow${ext}`]: 'sha256:' + 'a'.repeat(64),
507
- [`fallow-lsp${ext}`]: 'sha256:' + 'a'.repeat(64),
508
- [`fallow-mcp${ext}`]: 'sha256:' + 'a'.repeat(64),
511
+ [`fallow${ext}`]: "sha256:" + "a".repeat(64),
512
+ [`fallow-lsp${ext}`]: "sha256:" + "a".repeat(64),
513
+ [`fallow-mcp${ext}`]: "sha256:" + "a".repeat(64),
509
514
  },
510
515
  });
511
516
  const result = await verifyInstalled({
512
517
  dirOverride: dir,
513
518
  verifyFn: (p) => _verifyWithKey(p, rawPub),
514
- digestProvider: () => Promise.reject(new Error('should not be reached')),
519
+ digestProvider: () => Promise.reject(new Error("should not be reached")),
515
520
  });
516
521
  assert.equal(result.ok, false);
517
- assert.equal(result.code, 'digest-mismatch');
522
+ assert.equal(result.code, "digest-mismatch");
518
523
  });
519
524
 
520
- test('verifyInstalled ignores skip env when allowSkipEnv is false', async (t) => {
525
+ test("verifyInstalled ignores skip env when allowSkipEnv is false", async (t) => {
521
526
  const previous = process.env[SKIP_ENV];
522
- process.env[SKIP_ENV] = '1';
527
+ process.env[SKIP_ENV] = "1";
523
528
  t.after(() => {
524
529
  if (previous === undefined) delete process.env[SKIP_ENV];
525
530
  else process.env[SKIP_ENV] = previous;
@@ -536,3 +541,151 @@ test('verifyInstalled ignores skip env when allowSkipEnv is false', async (t) =>
536
541
  assert.equal(result.ok, true);
537
542
  assert.notEqual(result.skipped, true);
538
543
  });
544
+
545
+ // ---- verifyInstalledSync (lazy first-run path) ----------------------------
546
+
547
+ function makeSyncDigestProvider(_dir) {
548
+ return ({ binaryPath }) =>
549
+ "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex");
550
+ }
551
+
552
+ test("verifyInstalledSync with embedded digests returns ok end-to-end", (t) => {
553
+ const { privateKey, rawPub } = makeKeypair();
554
+ const dir = makePlatformDir(privateKey);
555
+ t.after(() => cleanup(dir));
556
+ writeManifest(dir, {
557
+ name: "@fallow-cli/x",
558
+ version: "9.9.9",
559
+ fallowDigests: computeDigestsForDir(dir),
560
+ });
561
+ const result = verifyInstalledSync({
562
+ dirOverride: dir,
563
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
564
+ });
565
+ assert.equal(result.ok, true);
566
+ assert.equal(result.package, "<override>");
567
+ });
568
+
569
+ test("verifyInstalledSync fails fast on first bad signature", (t) => {
570
+ const { privateKey, rawPub } = makeKeypair();
571
+ const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow-lsp" });
572
+ t.after(() => cleanup(dir));
573
+ writeManifest(dir, {
574
+ name: "@fallow-cli/x",
575
+ version: "9.9.9",
576
+ fallowDigests: computeDigestsForDir(dir),
577
+ });
578
+ const result = verifyInstalledSync({
579
+ dirOverride: dir,
580
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
581
+ });
582
+ assert.equal(result.ok, false);
583
+ assert.equal(result.code, "sig-invalid");
584
+ assert.match(result.binary, /fallow-lsp/);
585
+ });
586
+
587
+ test("verifyInstalledSync reports digest-mismatch when bytes diverge from embedded digest", (t) => {
588
+ const { privateKey, rawPub } = makeKeypair();
589
+ const dir = makePlatformDir(privateKey);
590
+ t.after(() => cleanup(dir));
591
+ writeManifest(dir, {
592
+ name: "@fallow-cli/x",
593
+ version: "9.9.9",
594
+ fallowDigests: {
595
+ fallow: "sha256:" + "a".repeat(64),
596
+ "fallow-lsp": "sha256:" + "a".repeat(64),
597
+ "fallow-mcp": "sha256:" + "a".repeat(64),
598
+ "fallow.exe": "sha256:" + "a".repeat(64),
599
+ "fallow-lsp.exe": "sha256:" + "a".repeat(64),
600
+ "fallow-mcp.exe": "sha256:" + "a".repeat(64),
601
+ },
602
+ });
603
+ const result = verifyInstalledSync({
604
+ dirOverride: dir,
605
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
606
+ });
607
+ assert.equal(result.ok, false);
608
+ assert.equal(result.code, "digest-mismatch");
609
+ });
610
+
611
+ test("verifyInstalledSync reports digest-unavailable when no embedded digest and no provider", (t) => {
612
+ const { privateKey, rawPub } = makeKeypair();
613
+ const dir = makePlatformDir(privateKey);
614
+ t.after(() => cleanup(dir));
615
+ writeManifest(dir, { name: "@fallow-cli/x", version: "9.9.9" });
616
+ const result = verifyInstalledSync({
617
+ dirOverride: dir,
618
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
619
+ });
620
+ assert.equal(result.ok, false);
621
+ assert.equal(result.code, "digest-unavailable");
622
+ assert.match(result.message, /predates fallow 2\.78\.1/);
623
+ assert.match(result.message, new RegExp(SKIP_ENV));
624
+ });
625
+
626
+ test("verifyInstalledSync accepts a sync digestProvider for test isolation", (t) => {
627
+ const { privateKey, rawPub } = makeKeypair();
628
+ const dir = makePlatformDir(privateKey);
629
+ t.after(() => cleanup(dir));
630
+ // No fallowDigests on manifest; the sync provider supplies them.
631
+ writeManifest(dir, { name: "@fallow-cli/x", version: "9.9.9" });
632
+ const result = verifyInstalledSync({
633
+ dirOverride: dir,
634
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
635
+ digestProvider: makeSyncDigestProvider(dir),
636
+ });
637
+ assert.equal(result.ok, true);
638
+ });
639
+
640
+ test("verifyInstalledSync surfaces provider errors as digest-unavailable", (t) => {
641
+ const { privateKey, rawPub } = makeKeypair();
642
+ const dir = makePlatformDir(privateKey);
643
+ t.after(() => cleanup(dir));
644
+ writeManifest(dir, { name: "@fallow-cli/x", version: "9.9.9" });
645
+ const result = verifyInstalledSync({
646
+ dirOverride: dir,
647
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
648
+ digestProvider: () => {
649
+ throw new Error("disk on fire");
650
+ },
651
+ });
652
+ assert.equal(result.ok, false);
653
+ assert.equal(result.code, "digest-unavailable");
654
+ assert.match(result.message, /disk on fire/);
655
+ });
656
+
657
+ test("verifyInstalledSync honors FALLOW_SKIP_BINARY_VERIFY", (t) => {
658
+ const previous = process.env[SKIP_ENV];
659
+ process.env[SKIP_ENV] = "1";
660
+ t.after(() => {
661
+ if (previous === undefined) delete process.env[SKIP_ENV];
662
+ else process.env[SKIP_ENV] = previous;
663
+ });
664
+ const result = verifyInstalledSync({ dirOverride: "/does/not/exist" });
665
+ assert.equal(result.ok, true);
666
+ assert.equal(result.skipped, true);
667
+ });
668
+
669
+ test("verifyInstalledSync ignores skip env when allowSkipEnv is false", (t) => {
670
+ const previous = process.env[SKIP_ENV];
671
+ process.env[SKIP_ENV] = "1";
672
+ t.after(() => {
673
+ if (previous === undefined) delete process.env[SKIP_ENV];
674
+ else process.env[SKIP_ENV] = previous;
675
+ });
676
+ const { privateKey, rawPub } = makeKeypair();
677
+ const dir = makePlatformDir(privateKey);
678
+ t.after(() => cleanup(dir));
679
+ writeManifest(dir, {
680
+ name: "@fallow-cli/x",
681
+ version: "9.9.9",
682
+ fallowDigests: computeDigestsForDir(dir),
683
+ });
684
+ const result = verifyInstalledSync({
685
+ dirOverride: dir,
686
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
687
+ allowSkipEnv: false,
688
+ });
689
+ assert.equal(result.ok, true);
690
+ assert.notEqual(result.skipped, true);
691
+ });