fallow 2.77.0 → 2.78.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/package.json +9 -9
- package/schema.json +1 -1
- package/scripts/verify-binary.js +69 -20
- package/scripts/verify-binary.test.js +136 -0
- package/types/output-contract.d.ts +32 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fallow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.78.1",
|
|
4
4
|
"description": "Codebase intelligence for TypeScript and JavaScript. Finds unused code, duplication, circular dependencies, complexity hotspots, and architecture drift. Optional runtime intelligence layer (Fallow Runtime) adds production execution evidence. Rust-native, sub-second, 95 framework plugins.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -74,13 +74,13 @@
|
|
|
74
74
|
"@tanstack/intent": "0.0.41"
|
|
75
75
|
},
|
|
76
76
|
"optionalDependencies": {
|
|
77
|
-
"@fallow-cli/darwin-arm64": "2.
|
|
78
|
-
"@fallow-cli/darwin-x64": "2.
|
|
79
|
-
"@fallow-cli/linux-x64-gnu": "2.
|
|
80
|
-
"@fallow-cli/linux-arm64-gnu": "2.
|
|
81
|
-
"@fallow-cli/linux-x64-musl": "2.
|
|
82
|
-
"@fallow-cli/linux-arm64-musl": "2.
|
|
83
|
-
"@fallow-cli/win32-arm64-msvc": "2.
|
|
84
|
-
"@fallow-cli/win32-x64-msvc": "2.
|
|
77
|
+
"@fallow-cli/darwin-arm64": "2.78.1",
|
|
78
|
+
"@fallow-cli/darwin-x64": "2.78.1",
|
|
79
|
+
"@fallow-cli/linux-x64-gnu": "2.78.1",
|
|
80
|
+
"@fallow-cli/linux-arm64-gnu": "2.78.1",
|
|
81
|
+
"@fallow-cli/linux-x64-musl": "2.78.1",
|
|
82
|
+
"@fallow-cli/linux-arm64-musl": "2.78.1",
|
|
83
|
+
"@fallow-cli/win32-arm64-msvc": "2.78.1",
|
|
84
|
+
"@fallow-cli/win32-x64-msvc": "2.78.1"
|
|
85
85
|
}
|
|
86
86
|
}
|
package/schema.json
CHANGED
|
@@ -1142,7 +1142,7 @@
|
|
|
1142
1142
|
"type": "object",
|
|
1143
1143
|
"properties": {
|
|
1144
1144
|
"sdkPatterns": {
|
|
1145
|
-
"description": "Additional SDK call patterns to detect as feature flags.\nThese are merged with the built-in patterns
|
|
1145
|
+
"description": "Additional SDK call patterns to detect as feature flags.\nThese are merged with the built-in patterns for common providers\nincluding LaunchDarkly, Statsig, Unleash, GrowthBook, Split, PostHog,\nVercel Flags, ConfigCat, Flagsmith, Optimizely, and Eppo.",
|
|
1146
1146
|
"type": "array",
|
|
1147
1147
|
"items": {
|
|
1148
1148
|
"$ref": "#/$defs/SdkPattern"
|
package/scripts/verify-binary.js
CHANGED
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Verifies each platform binary against a .sig file shipped alongside it in
|
|
4
4
|
// the @fallow-cli/<platform> package, then cross-checks the binary bytes
|
|
5
|
-
// against
|
|
6
|
-
//
|
|
7
|
-
// `.github/scripts/sign-binary.mjs` using the workflow's
|
|
5
|
+
// against an expected SHA-256 digest. The .sig is produced at release time
|
|
6
|
+
// by `.github/scripts/sign-binary.mjs` using the workflow's
|
|
8
7
|
// ED25519_BINARY_SIGNING_PRIVATE_KEY secret. The matching public key (32 raw
|
|
9
8
|
// bytes) is embedded below and is identical to the value already trusted by
|
|
10
9
|
// the VS Code extension at editors/vscode/src/download.ts:19-22.
|
|
11
10
|
//
|
|
11
|
+
// SHA-256 digest source (in order of preference, refs #465 and #597):
|
|
12
|
+
// 1. `fallowDigests` field in the platform package's package.json, written
|
|
13
|
+
// at release time by the `npm-prep` job. This is the steady-state path:
|
|
14
|
+
// no network traffic, immune to GitHub API rate limits.
|
|
15
|
+
// 2. Fallback: GitHub Release asset digest via the public REST API. Kept
|
|
16
|
+
// for backwards compatibility with platform packages published before
|
|
17
|
+
// #597 that lack the embedded field. Shared-IP CI runners (Buildkite,
|
|
18
|
+
// pooled GHA runners) can exceed the 60 req/hr unauthenticated limit;
|
|
19
|
+
// that failure mode is what motivated #597.
|
|
20
|
+
//
|
|
12
21
|
// Triggered from scripts/postinstall.js and from the GitHub Action installer
|
|
13
22
|
// at action/scripts/install.sh. The escape hatch FALLOW_SKIP_BINARY_VERIFY=1
|
|
14
23
|
// is documented in SECURITY.md.
|
|
15
24
|
//
|
|
16
|
-
// No external dependencies: uses node:crypto and node:fs only.
|
|
25
|
+
// No external dependencies: uses node:crypto and node:fs only.
|
|
17
26
|
|
|
18
27
|
const crypto = require('node:crypto');
|
|
19
28
|
const fs = require('node:fs');
|
|
@@ -211,6 +220,31 @@ function platformPackageDir(pkg, resolveFrom) {
|
|
|
211
220
|
return { dir: path.dirname(manifestPath), manifestPath };
|
|
212
221
|
}
|
|
213
222
|
|
|
223
|
+
// Read the SHA-256 digest for a binary embedded in the platform package's
|
|
224
|
+
// package.json (written by the npm-prep job at release time as
|
|
225
|
+
// `fallowDigests[<filename>]`). Returns the normalized digest hex or null
|
|
226
|
+
// when the manifest does not exist, cannot be parsed, lacks the field, or
|
|
227
|
+
// the value is malformed. Refs #597.
|
|
228
|
+
function readEmbeddedDigest(manifestPath, binaryFileName) {
|
|
229
|
+
if (typeof manifestPath !== 'string' || manifestPath.length === 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
let manifest;
|
|
233
|
+
try {
|
|
234
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const digests = manifest.fallowDigests;
|
|
242
|
+
if (!digests || typeof digests !== 'object') {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return normalizeDigest(digests[binaryFileName]);
|
|
246
|
+
}
|
|
247
|
+
|
|
214
248
|
function binaryTargetsForPlatform(platformId) {
|
|
215
249
|
const isWindows = process.platform === 'win32';
|
|
216
250
|
const ext = isWindows ? '.exe' : '';
|
|
@@ -265,6 +299,9 @@ async function verifyInstalled(options) {
|
|
|
265
299
|
pkg = '<override>';
|
|
266
300
|
version = opts.version || '0.0.0';
|
|
267
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');
|
|
268
305
|
} else {
|
|
269
306
|
if (process.platform !== 'linux') {
|
|
270
307
|
pkg = getPlatformPackage(process.platform, process.arch);
|
|
@@ -308,22 +345,33 @@ async function verifyInstalled(options) {
|
|
|
308
345
|
if (!result.ok) {
|
|
309
346
|
return { ...result, binary: binaryPath, package: pkg };
|
|
310
347
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
}
|
|
327
375
|
}
|
|
328
376
|
const digestResult = verifyDigestAt(binaryPath, expectedDigest);
|
|
329
377
|
if (!digestResult.ok) {
|
|
@@ -340,6 +388,7 @@ module.exports = {
|
|
|
340
388
|
_verifyWithKey,
|
|
341
389
|
fetchReleaseDigest,
|
|
342
390
|
normalizeDigest,
|
|
391
|
+
readEmbeddedDigest,
|
|
343
392
|
sha256Hex,
|
|
344
393
|
EMBEDDED_PUBLIC_KEY,
|
|
345
394
|
ED25519_SPKI_HEADER,
|
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
verifyInstalled,
|
|
13
13
|
sha256Hex,
|
|
14
14
|
normalizeDigest,
|
|
15
|
+
readEmbeddedDigest,
|
|
15
16
|
EMBEDDED_PUBLIC_KEY,
|
|
16
17
|
ED25519_SPKI_HEADER,
|
|
17
18
|
SKIP_ENV,
|
|
@@ -381,6 +382,141 @@ test('verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY', async (t) => {
|
|
|
381
382
|
assert.equal(result.skipped, true);
|
|
382
383
|
});
|
|
383
384
|
|
|
385
|
+
function computeDigestsForDir(dir) {
|
|
386
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
387
|
+
const out = {};
|
|
388
|
+
for (const base of ['fallow', 'fallow-lsp', 'fallow-mcp']) {
|
|
389
|
+
const fileName = `${base}${ext}`;
|
|
390
|
+
const full = path.join(dir, fileName);
|
|
391
|
+
out[fileName] = 'sha256:' + crypto.createHash('sha256').update(fs.readFileSync(full)).digest('hex');
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function writeManifest(dir, body) {
|
|
397
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(body));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
test('readEmbeddedDigest returns null when manifest is missing', () => {
|
|
401
|
+
assert.equal(readEmbeddedDigest('/does/not/exist/package.json', 'fallow'), null);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('readEmbeddedDigest returns null when fallowDigests field is absent', (t) => {
|
|
405
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fallow-vbtest-emb-'));
|
|
406
|
+
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);
|
|
409
|
+
});
|
|
410
|
+
|
|
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-'));
|
|
413
|
+
t.after(() => cleanup(dir));
|
|
414
|
+
writeManifest(dir, {
|
|
415
|
+
name: '@fallow-cli/x',
|
|
416
|
+
version: '1.0.0',
|
|
417
|
+
fallowDigests: { fallow: 'not-a-real-digest' },
|
|
418
|
+
});
|
|
419
|
+
assert.equal(readEmbeddedDigest(path.join(dir, 'package.json'), 'fallow'), null);
|
|
420
|
+
});
|
|
421
|
+
|
|
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-'));
|
|
424
|
+
t.after(() => cleanup(dir));
|
|
425
|
+
const hex = 'a'.repeat(64);
|
|
426
|
+
writeManifest(dir, {
|
|
427
|
+
name: '@fallow-cli/x',
|
|
428
|
+
version: '1.0.0',
|
|
429
|
+
fallowDigests: { fallow: 'sha256:' + hex },
|
|
430
|
+
});
|
|
431
|
+
assert.equal(readEmbeddedDigest(path.join(dir, 'package.json'), 'fallow'), hex);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('verifyInstalled uses the embedded digest without calling the provider', async (t) => {
|
|
435
|
+
const { privateKey, rawPub } = makeKeypair();
|
|
436
|
+
const dir = makePlatformDir(privateKey);
|
|
437
|
+
t.after(() => cleanup(dir));
|
|
438
|
+
writeManifest(dir, {
|
|
439
|
+
name: '@fallow-cli/x',
|
|
440
|
+
version: '1.0.0',
|
|
441
|
+
fallowDigests: computeDigestsForDir(dir),
|
|
442
|
+
});
|
|
443
|
+
let providerCalls = 0;
|
|
444
|
+
const result = await verifyInstalled({
|
|
445
|
+
dirOverride: dir,
|
|
446
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
447
|
+
digestProvider: () => {
|
|
448
|
+
providerCalls += 1;
|
|
449
|
+
return Promise.reject(new Error('provider should not be called'));
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
assert.equal(result.ok, true, JSON.stringify(result));
|
|
453
|
+
assert.equal(providerCalls, 0);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('verifyInstalled falls back to the provider when the embedded digest is missing', async (t) => {
|
|
457
|
+
const { privateKey, rawPub } = makeKeypair();
|
|
458
|
+
const dir = makePlatformDir(privateKey);
|
|
459
|
+
t.after(() => cleanup(dir));
|
|
460
|
+
// No package.json at all; legacy platform package shape.
|
|
461
|
+
let providerCalls = 0;
|
|
462
|
+
const result = await verifyInstalled({
|
|
463
|
+
dirOverride: dir,
|
|
464
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
465
|
+
digestProvider: ({ binaryPath }) => {
|
|
466
|
+
providerCalls += 1;
|
|
467
|
+
return Promise.resolve('sha256:' + crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'));
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
assert.equal(result.ok, true);
|
|
471
|
+
assert.equal(providerCalls, 3);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('verifyInstalled falls back to the provider when fallowDigests is partial / malformed', async (t) => {
|
|
475
|
+
const { privateKey, rawPub } = makeKeypair();
|
|
476
|
+
const dir = makePlatformDir(privateKey);
|
|
477
|
+
t.after(() => cleanup(dir));
|
|
478
|
+
writeManifest(dir, {
|
|
479
|
+
name: '@fallow-cli/x',
|
|
480
|
+
version: '1.0.0',
|
|
481
|
+
fallowDigests: { fallow: 'not-a-real-digest' },
|
|
482
|
+
});
|
|
483
|
+
let providerCalls = 0;
|
|
484
|
+
const result = await verifyInstalled({
|
|
485
|
+
dirOverride: dir,
|
|
486
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
487
|
+
digestProvider: ({ binaryPath }) => {
|
|
488
|
+
providerCalls += 1;
|
|
489
|
+
return Promise.resolve('sha256:' + crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'));
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
assert.equal(result.ok, true);
|
|
493
|
+
// One call per binary because all three entries fail to normalize.
|
|
494
|
+
assert.equal(providerCalls, 3);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('verifyInstalled returns digest-mismatch when the embedded digest disagrees with the binary', async (t) => {
|
|
498
|
+
const { privateKey, rawPub } = makeKeypair();
|
|
499
|
+
const dir = makePlatformDir(privateKey);
|
|
500
|
+
t.after(() => cleanup(dir));
|
|
501
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
502
|
+
writeManifest(dir, {
|
|
503
|
+
name: '@fallow-cli/x',
|
|
504
|
+
version: '1.0.0',
|
|
505
|
+
fallowDigests: {
|
|
506
|
+
[`fallow${ext}`]: 'sha256:' + 'a'.repeat(64),
|
|
507
|
+
[`fallow-lsp${ext}`]: 'sha256:' + 'a'.repeat(64),
|
|
508
|
+
[`fallow-mcp${ext}`]: 'sha256:' + 'a'.repeat(64),
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
const result = await verifyInstalled({
|
|
512
|
+
dirOverride: dir,
|
|
513
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
514
|
+
digestProvider: () => Promise.reject(new Error('should not be reached')),
|
|
515
|
+
});
|
|
516
|
+
assert.equal(result.ok, false);
|
|
517
|
+
assert.equal(result.code, 'digest-mismatch');
|
|
518
|
+
});
|
|
519
|
+
|
|
384
520
|
test('verifyInstalled ignores skip env when allowSkipEnv is false', async (t) => {
|
|
385
521
|
const previous = process.env[SKIP_ENV];
|
|
386
522
|
process.env[SKIP_ENV] = '1';
|
|
@@ -2169,6 +2169,12 @@ export interface Meta {
|
|
|
2169
2169
|
* URL to the documentation page for this command.
|
|
2170
2170
|
*/
|
|
2171
2171
|
docs?: (string | null)
|
|
2172
|
+
/**
|
|
2173
|
+
* Per-field definitions for envelope fields and action payload fields.
|
|
2174
|
+
*/
|
|
2175
|
+
field_definitions?: {
|
|
2176
|
+
[k: string]: string
|
|
2177
|
+
}
|
|
2172
2178
|
/**
|
|
2173
2179
|
* Per-metric definitions: name, description, range, interpretation.
|
|
2174
2180
|
*/
|
|
@@ -4033,15 +4039,15 @@ export interface TargetEvidence {
|
|
|
4033
4039
|
/**
|
|
4034
4040
|
* Names of unused exports (populated for `RemoveDeadCode` targets).
|
|
4035
4041
|
*/
|
|
4036
|
-
unused_exports
|
|
4042
|
+
unused_exports?: string[]
|
|
4037
4043
|
/**
|
|
4038
4044
|
* Complex functions with line numbers and cognitive scores (populated for `ExtractComplexFunctions`).
|
|
4039
4045
|
*/
|
|
4040
|
-
complex_functions
|
|
4046
|
+
complex_functions?: EvidenceFunction[]
|
|
4041
4047
|
/**
|
|
4042
4048
|
* Files forming the import cycle (populated for `BreakCircularDependency` targets).
|
|
4043
4049
|
*/
|
|
4044
|
-
cycle_path
|
|
4050
|
+
cycle_path?: string[]
|
|
4045
4051
|
}
|
|
4046
4052
|
/**
|
|
4047
4053
|
* A function referenced in target evidence.
|
|
@@ -5479,6 +5485,12 @@ export interface CombinedOutput {
|
|
|
5479
5485
|
schema_version: SchemaVersion
|
|
5480
5486
|
version: ToolVersion
|
|
5481
5487
|
elapsed_ms: ElapsedMs
|
|
5488
|
+
/**
|
|
5489
|
+
* Sectioned `_meta` block emitted only when `--explain` is passed.
|
|
5490
|
+
* Contains `check`, `dupes`, and/or `health` keys matching the analyses
|
|
5491
|
+
* enabled for the combined run.
|
|
5492
|
+
*/
|
|
5493
|
+
_meta?: (CombinedMeta | null)
|
|
5482
5494
|
/**
|
|
5483
5495
|
* Dead-code analysis sub-envelope. Absent when `--skip check`.
|
|
5484
5496
|
*/
|
|
@@ -5496,6 +5508,23 @@ dupes?: (DupesReportPayload | null)
|
|
|
5496
5508
|
*/
|
|
5497
5509
|
health?: (HealthReport | null)
|
|
5498
5510
|
}
|
|
5511
|
+
/**
|
|
5512
|
+
* Sectioned `_meta` block for the bare combined JSON envelope.
|
|
5513
|
+
*/
|
|
5514
|
+
export interface CombinedMeta {
|
|
5515
|
+
/**
|
|
5516
|
+
* Dead-code metadata from `crate::explain::check_meta()`.
|
|
5517
|
+
*/
|
|
5518
|
+
check?: (Meta | null)
|
|
5519
|
+
/**
|
|
5520
|
+
* Duplication metadata from `crate::explain::dupes_meta()`.
|
|
5521
|
+
*/
|
|
5522
|
+
dupes?: (Meta | null)
|
|
5523
|
+
/**
|
|
5524
|
+
* Health metadata from `crate::explain::health_meta()`.
|
|
5525
|
+
*/
|
|
5526
|
+
health?: (Meta | null)
|
|
5527
|
+
}
|
|
5499
5528
|
/**
|
|
5500
5529
|
* Single CodeClimate-compatible issue inside [`CodeClimateOutput`].
|
|
5501
5530
|
*/
|