@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 CHANGED
@@ -77,9 +77,32 @@ reads and leaves.
77
77
  ## Tests
78
78
 
79
79
  ```
80
- node --test test/core.test.mjs # 32 tests, no dependencies
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 # 32 项,无依赖
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.0",
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",
@@ -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;
@@ -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
- const bytes = Buffer.from(await response.arrayBuffer());
355
- const integrityOk = meta.dist.shasum ? sha1(bytes) === meta.dist.shasum : null;
356
- return { name, version: meta.version, bytes, meta, integrityOk };
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
- const comparison = compareArtifact(artifact, tree);
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
  })