@cyd-prc/dsh-audit-chain 0.1.0 → 0.1.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/README.md +24 -1
- package/README.zh-CN.md +19 -1
- package/lib/chain.js +9 -0
- package/package.json +1 -1
- package/test/core.test.mjs +27 -0
- package/tools/verify-release.mjs +69 -7
package/README.md
CHANGED
|
@@ -77,9 +77,32 @@ reads and leaves.
|
|
|
77
77
|
## Tests
|
|
78
78
|
|
|
79
79
|
```
|
|
80
|
-
node --test test/core.test.mjs #
|
|
80
|
+
node --test test/core.test.mjs # 33 tests, no dependencies
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
## Changelog
|
|
84
|
+
|
|
85
|
+
### 0.1.1
|
|
86
|
+
|
|
87
|
+
- **`ChainLog.readAll()`** — the reader shape the shape-guard side historically
|
|
88
|
+
used (`{ entries, corrupt }`), so the migration's adapter layer stays a pure
|
|
89
|
+
re-export.
|
|
90
|
+
- **`verify-release` checks the root packument (M6, found by the test side).**
|
|
91
|
+
The version document and the tarball can both be 200 while
|
|
92
|
+
`npm install <name>` still fails — the root packument propagates later
|
|
93
|
+
(measured: ~2.5 minutes behind on a fresh package). A `--spec` verification is
|
|
94
|
+
no longer green in that window, and a `dist-tags.latest` mismatch is printed
|
|
95
|
+
as a note (verifying an older release is ordinary work).
|
|
96
|
+
- **`verify-release` understands `bundleDependencies`.** Bundled copies leave
|
|
97
|
+
the tree comparison (they are someone else's package) but their versions are
|
|
98
|
+
the release's contract: each bundled copy must match an *exact* pin in
|
|
99
|
+
`dependencies` — a range makes the artifact's bytes depend on when it was
|
|
100
|
+
packed. This is the shape the three plugins' migration to this package takes.
|
|
101
|
+
|
|
102
|
+
### 0.1.0
|
|
103
|
+
|
|
104
|
+
- First cut, extracted from the two drifted copies. See the provenance section.
|
|
105
|
+
|
|
83
106
|
The suite is the union of the anchors that pinned both historical
|
|
84
107
|
implementations (entropy-guard defects 14/15/16/19/20, shape-guard I7–I13 /
|
|
85
108
|
S1–S4), plus the byte-compatibility pin.
|
package/README.zh-CN.md
CHANGED
|
@@ -68,9 +68,27 @@ node tools/verify-release.mjs --spec @cyd-prc/dsh-audit-chain@<v> --run-tests --
|
|
|
68
68
|
## 测试
|
|
69
69
|
|
|
70
70
|
```
|
|
71
|
-
node --test test/core.test.mjs #
|
|
71
|
+
node --test test/core.test.mjs # 33 项,无依赖
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
## 变更节
|
|
75
|
+
|
|
76
|
+
### 0.1.1
|
|
77
|
+
|
|
78
|
+
- **`ChainLog.readAll()`**——shape-guard 一侧历史上使用的读取形状(`{ entries, corrupt }`),
|
|
79
|
+
迁移时的适配层因此是纯 re-export。
|
|
80
|
+
- **`verify-release` 现在检查根 packument(M6,测试侧咬出)。** 版本文档与 tarball 都已 200 时
|
|
81
|
+
`npm install <name>` 仍可能失败——根文档传播更慢(实测:新包晚约 2.5 分钟)。这个窗口内
|
|
82
|
+
`--spec` 校验不再报绿;`dist-tags.latest` 与被验版本不一致时打印一条 note
|
|
83
|
+
(验旧版本是正常工作,不算失败)。
|
|
84
|
+
- **`verify-release` 认识 `bundleDependencies`。** 内嵌副本退出树比较(那是别人家的包),
|
|
85
|
+
但它们的版本是本发布的契约:每个内嵌副本必须与 `dependencies` 里的**精确**钉版一致——
|
|
86
|
+
区间会让实件的字节取决于打包的时间。这正是三个插件迁移到本包时采取的形状。
|
|
87
|
+
|
|
88
|
+
### 0.1.0
|
|
89
|
+
|
|
90
|
+
- 首版,从两份漂移的副本抽出。见溯源节。
|
|
91
|
+
|
|
74
92
|
套件是两份历史实现的锚的并集(entropy-guard 缺陷 14/15/16/19/20、shape-guard
|
|
75
93
|
I7–I13 / S1–S4),外加字节兼容针。
|
|
76
94
|
|
package/lib/chain.js
CHANGED
|
@@ -466,6 +466,15 @@ export class ChainLog {
|
|
|
466
466
|
return this._readAll().map((entry) => structuredClone(entry));
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Every entry plus the corrupt-line count — the reader shape the shape-guard
|
|
471
|
+
* side historically used (`readAll().entries`).
|
|
472
|
+
* @returns {{ entries: Record<string, unknown>[], corrupt: number }} the read.
|
|
473
|
+
*/
|
|
474
|
+
readAll() {
|
|
475
|
+
return { entries: this.entries, corrupt: this.corruptLines };
|
|
476
|
+
}
|
|
477
|
+
|
|
469
478
|
/** @returns {Record<string, unknown>[]} the cached read. */
|
|
470
479
|
_readAll() {
|
|
471
480
|
if (this.path !== null) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyd-prc/dsh-audit-chain",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The one tamper-evident JSONL audit chain every dsh guard shares: a multi-writer-safe writer (exclusive lock, fresh tail, torn-tail termination) and the chain-grading contract verifier. Extracted from dsh-entropy-guard and dsh-shape-guard, where the copies drifted; one chain, one implementation.",
|
|
6
6
|
"type": "module",
|
package/test/core.test.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
verifyChain,
|
|
34
34
|
} from '../index.js';
|
|
35
35
|
import {
|
|
36
|
+
checkBundledDependencies,
|
|
36
37
|
jsonEquivalent,
|
|
37
38
|
looksLikeText,
|
|
38
39
|
matchesReference,
|
|
@@ -339,6 +340,10 @@ describe('grading (the contract, pinned)', () => {
|
|
|
339
340
|
assert.equal(reading.entries, 1);
|
|
340
341
|
assert.equal(reading.corrupt, 1);
|
|
341
342
|
assert.equal(reading.status, 'verified', 'clause 8: corrupt alone never grades hostile');
|
|
343
|
+
// The reader shape the shape-guard side historically used.
|
|
344
|
+
const all = new ChainLog(path).readAll();
|
|
345
|
+
assert.equal(all.entries.length, 1);
|
|
346
|
+
assert.equal(all.corrupt, 1);
|
|
342
347
|
} finally {
|
|
343
348
|
rmSync(dir, { recursive: true, force: true });
|
|
344
349
|
}
|
|
@@ -423,6 +428,28 @@ describe('release tooling', () => {
|
|
|
423
428
|
assert.deepEqual(scanShippedReferences(artifact).unresolved, [{ file: 'lib/tool.mjs', reference: 'DESIGN-v1.md' }]);
|
|
424
429
|
});
|
|
425
430
|
|
|
431
|
+
it('checks bundled dependencies against their exact pins', () => {
|
|
432
|
+
const manifest = {
|
|
433
|
+
bundleDependencies: ['@cyd-prc/dsh-audit-chain'],
|
|
434
|
+
dependencies: { '@cyd-prc/dsh-audit-chain': '0.1.0' },
|
|
435
|
+
};
|
|
436
|
+
const packed = (version) => readTarGz(buildTarGz({
|
|
437
|
+
'package/index.js': 'export {};\n',
|
|
438
|
+
'package/node_modules/@cyd-prc/dsh-audit-chain/package.json': JSON.stringify({ name: '@cyd-prc/dsh-audit-chain', version }),
|
|
439
|
+
}));
|
|
440
|
+
const ok = checkBundledDependencies(manifest, packed('0.1.0'));
|
|
441
|
+
assert.equal(ok.mismatches, 0);
|
|
442
|
+
assert.equal(ok.excluded.length, 1, 'the bundled copy leaves the tree comparison');
|
|
443
|
+
assert.equal(checkBundledDependencies(manifest, packed('0.0.9')).mismatches, 1, 'a drifted bundled copy fails');
|
|
444
|
+
const unpinned = checkBundledDependencies(
|
|
445
|
+
{ bundleDependencies: ['x'], dependencies: { x: '^1.0.0' } },
|
|
446
|
+
readTarGz(buildTarGz({ 'package/index.js': '', 'package/node_modules/x/package.json': '{"name":"x","version":"1.2.3"}' })),
|
|
447
|
+
);
|
|
448
|
+
assert.equal(unpinned.mismatches, 1, 'a range makes the artifact non-deterministic');
|
|
449
|
+
const absent = checkBundledDependencies(manifest, readTarGz(buildTarGz({ 'package/index.js': '' })));
|
|
450
|
+
assert.equal(absent.mismatches, 1, 'a declared bundle missing from the artifact fails');
|
|
451
|
+
});
|
|
452
|
+
|
|
426
453
|
it('names the tree HEAD it compared against', () => {
|
|
427
454
|
const probe = spawnSync('git', ['rev-parse', '--short=7', 'HEAD'], { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
428
455
|
const expected = probe.status === 0 ? probe.stdout.trim() : null;
|
package/tools/verify-release.mjs
CHANGED
|
@@ -350,12 +350,24 @@ async function fetchArtifact(spec, waitSeconds = 0) {
|
|
|
350
350
|
lastProblem = `${name}@${version} is not in the registry yet`;
|
|
351
351
|
} else {
|
|
352
352
|
const response = await fetch(tarball);
|
|
353
|
-
if (response.ok) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
lastProblem = `the tarball is not fetchable yet (HTTP ${response.status})`;
|
|
355
|
+
} else {
|
|
356
|
+
// M6: the version document and the tarball being 200 is not enough —
|
|
357
|
+
// `npm install <name>` enters through the ROOT packument, which
|
|
358
|
+
// propagates later (measured on a fresh package: ~2.5 minutes behind).
|
|
359
|
+
// A release is not verified while the name itself still 404s.
|
|
360
|
+
const packumentResponse = await fetch(base);
|
|
361
|
+
const packument = packumentResponse.ok ? await packumentResponse.json() : null;
|
|
362
|
+
if (packument?.versions?.[meta.version] === undefined) {
|
|
363
|
+
lastProblem = 'the root packument does not list it yet (install-by-name still fails)';
|
|
364
|
+
} else {
|
|
365
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
366
|
+
const integrityOk = meta.dist.shasum ? sha1(bytes) === meta.dist.shasum : null;
|
|
367
|
+
const latest = packument?.['dist-tags']?.latest ?? null;
|
|
368
|
+
return { name, version: meta.version, bytes, meta, integrityOk, latest };
|
|
369
|
+
}
|
|
357
370
|
}
|
|
358
|
-
lastProblem = `the tarball is not fetchable yet (HTTP ${response.status})`;
|
|
359
371
|
}
|
|
360
372
|
} catch (error) {
|
|
361
373
|
lastProblem = `registry read failed (${error?.message ?? error})`;
|
|
@@ -431,6 +443,35 @@ function runTestsInside(artifact) {
|
|
|
431
443
|
}
|
|
432
444
|
}
|
|
433
445
|
|
|
446
|
+
/**
|
|
447
|
+
* A package carrying `bundleDependencies` ships those deps inlined under
|
|
448
|
+
* `node_modules/` in the tarball. They are not part of the tree comparison —
|
|
449
|
+
* they are somebody else's package — but their *versions* are this release's
|
|
450
|
+
* contract: the bundled copy must match the declared spec, and the spec must be
|
|
451
|
+
* an exact pin (a range makes the artifact's contents depend on when it was
|
|
452
|
+
* packed).
|
|
453
|
+
* @param manifest - the tree's package.json, parsed.
|
|
454
|
+
* @param artifact - the readTarGz map.
|
|
455
|
+
* @returns `{ checked: [{name, spec, bundled, ok}], mismatches: number, excluded: string[] }`.
|
|
456
|
+
*/
|
|
457
|
+
export function checkBundledDependencies(manifest, artifact) {
|
|
458
|
+
const bundled = Array.isArray(manifest.bundleDependencies) ? manifest.bundleDependencies : [];
|
|
459
|
+
const checked = [];
|
|
460
|
+
let mismatches = 0;
|
|
461
|
+
for (const name of bundled) {
|
|
462
|
+
const spec = manifest.dependencies?.[name] ?? null;
|
|
463
|
+
const packedManifest = artifact.get(`node_modules/${name}/package.json`);
|
|
464
|
+
const bundledVersion = packedManifest === undefined ? null : JSON.parse(packedManifest.toString('utf8')).version;
|
|
465
|
+
const ok = spec !== null && spec === bundledVersion;
|
|
466
|
+
if (!ok) mismatches += 1;
|
|
467
|
+
checked.push({ name, spec, bundled: bundledVersion, ok });
|
|
468
|
+
}
|
|
469
|
+
const excluded = bundled.length === 0
|
|
470
|
+
? []
|
|
471
|
+
: [...artifact.keys()].filter((path) => bundled.some((name) => path.startsWith(`node_modules/${name}/`)));
|
|
472
|
+
return { checked, mismatches, excluded };
|
|
473
|
+
}
|
|
474
|
+
|
|
434
475
|
function main(argv) {
|
|
435
476
|
const options = { spec: null, pack: false, runTests: false, json: false, packer: null, wait: 0 };
|
|
436
477
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -489,10 +530,22 @@ function main(argv) {
|
|
|
489
530
|
if (fetched.integrityOk === false) {
|
|
490
531
|
process.stdout.write(`container integrity: MISMATCH (dist.shasum ${fetched.meta.dist.shasum})\n`);
|
|
491
532
|
}
|
|
533
|
+
// Informational, never a failure: verifying an older release while a
|
|
534
|
+
// newer one is `latest` is ordinary work.
|
|
535
|
+
if (fetched.latest !== null && fetched.latest !== fetched.version) {
|
|
536
|
+
process.stdout.write(`note: dist-tags.latest is ${fetched.latest}; you are verifying ${fetched.version}\n`);
|
|
537
|
+
}
|
|
492
538
|
}
|
|
493
539
|
const artifact = readTarGz(bytes);
|
|
494
540
|
const tree = walkTree(ROOT);
|
|
495
|
-
|
|
541
|
+
// Bundled dependencies ship inside the artifact under node_modules/: they
|
|
542
|
+
// are someone else's package, so they leave the tree comparison — but
|
|
543
|
+
// their versions are this release's contract.
|
|
544
|
+
const bundled = checkBundledDependencies(manifest, artifact);
|
|
545
|
+
const artifactForCompare = bundled.excluded.length === 0
|
|
546
|
+
? artifact
|
|
547
|
+
: new Map([...artifact].filter(([path]) => !bundled.excluded.includes(path)));
|
|
548
|
+
const comparison = compareArtifact(artifactForCompare, tree);
|
|
496
549
|
// The artifact's *own* metadata is what has to be self-consistent: a package
|
|
497
550
|
// whose manifest points at something it does not carry is broken wherever it
|
|
498
551
|
// is installed from, which is exactly what shipped in 0.2.2.
|
|
@@ -509,7 +562,7 @@ function main(argv) {
|
|
|
509
562
|
|
|
510
563
|
if (options.json) {
|
|
511
564
|
process.stdout.write(`${JSON.stringify({
|
|
512
|
-
source, spec, treeHead, files: artifact.size, comparison, references, codeReferences, localOnly, tests,
|
|
565
|
+
source, spec, treeHead, files: artifact.size, comparison, references, codeReferences, bundled, localOnly, tests,
|
|
513
566
|
}, null, 2)}\n`);
|
|
514
567
|
} else {
|
|
515
568
|
process.stdout.write(`verify-release: ${source} (${artifact.size} files) · tree ${treeHead ?? '(unknown HEAD)'}\n`);
|
|
@@ -529,6 +582,14 @@ function main(argv) {
|
|
|
529
582
|
process.stdout.write(` UNRESOLVED ${entry.file} -> ${entry.reference}\n`);
|
|
530
583
|
}
|
|
531
584
|
process.stdout.write(` in the tree but not in the artifact: ${localOnly.length === 0 ? '(none)' : localOnly.join(', ')}\n`);
|
|
585
|
+
if (bundled.checked.length > 0) {
|
|
586
|
+
process.stdout.write(` bundled dependencies: ${bundled.checked.length} mismatches: ${bundled.mismatches} (excluded from the tree comparison: ${bundled.excluded.length} files)\n`);
|
|
587
|
+
for (const entry of bundled.checked) {
|
|
588
|
+
if (!entry.ok) {
|
|
589
|
+
process.stdout.write(` BUNDLED-MISMATCH ${entry.name}: manifest pins ${entry.spec ?? '(undeclared)'}, the artifact carries ${entry.bundled ?? '(absent)'}\n`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
532
593
|
if (tests !== null) {
|
|
533
594
|
process.stdout.write(` suite inside the artifact: ${tests.ran ? `ran ${tests.testFile}, exit ${tests.status}` : tests.reason}\n`);
|
|
534
595
|
}
|
|
@@ -537,6 +598,7 @@ function main(argv) {
|
|
|
537
598
|
|| references.missing > 0
|
|
538
599
|
|| references.promisedButAbsent.length > 0
|
|
539
600
|
|| codeReferences.unresolved.length > 0
|
|
601
|
+
|| bundled.mismatches > 0
|
|
540
602
|
|| (tests !== null && tests.ran && tests.status !== 0);
|
|
541
603
|
return failed ? 1 : 0;
|
|
542
604
|
})
|