@mikeargento/bitgraph 1.1.0 → 1.2.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.
Files changed (195) hide show
  1. package/README.md +49 -40
  2. package/dist/__tests__/audit-anchors.test.d.ts +2 -0
  3. package/dist/__tests__/audit-anchors.test.d.ts.map +1 -0
  4. package/dist/__tests__/audit-anchors.test.js +152 -0
  5. package/dist/__tests__/audit-anchors.test.js.map +1 -0
  6. package/dist/__tests__/audit-anomalies.test.d.ts +2 -0
  7. package/dist/__tests__/audit-anomalies.test.d.ts.map +1 -0
  8. package/dist/__tests__/audit-anomalies.test.js +663 -0
  9. package/dist/__tests__/audit-anomalies.test.js.map +1 -0
  10. package/dist/__tests__/audit-attestation.test.d.ts +2 -0
  11. package/dist/__tests__/audit-attestation.test.d.ts.map +1 -0
  12. package/dist/__tests__/audit-attestation.test.js +403 -0
  13. package/dist/__tests__/audit-attestation.test.js.map +1 -0
  14. package/dist/__tests__/audit-bench-smoke.test.d.ts +2 -0
  15. package/dist/__tests__/audit-bench-smoke.test.d.ts.map +1 -0
  16. package/dist/__tests__/audit-bench-smoke.test.js +87 -0
  17. package/dist/__tests__/audit-bench-smoke.test.js.map +1 -0
  18. package/dist/__tests__/audit-boundary.test.d.ts +20 -0
  19. package/dist/__tests__/audit-boundary.test.d.ts.map +1 -0
  20. package/dist/__tests__/audit-boundary.test.js +84 -0
  21. package/dist/__tests__/audit-boundary.test.js.map +1 -0
  22. package/dist/__tests__/audit-cli.test.d.ts +2 -0
  23. package/dist/__tests__/audit-cli.test.d.ts.map +1 -0
  24. package/dist/__tests__/audit-cli.test.js +239 -0
  25. package/dist/__tests__/audit-cli.test.js.map +1 -0
  26. package/dist/__tests__/audit-dependencies.test.d.ts +2 -0
  27. package/dist/__tests__/audit-dependencies.test.d.ts.map +1 -0
  28. package/dist/__tests__/audit-dependencies.test.js +116 -0
  29. package/dist/__tests__/audit-dependencies.test.js.map +1 -0
  30. package/dist/__tests__/audit-fixtures.d.ts +265 -0
  31. package/dist/__tests__/audit-fixtures.d.ts.map +1 -0
  32. package/dist/__tests__/audit-fixtures.js +511 -0
  33. package/dist/__tests__/audit-fixtures.js.map +1 -0
  34. package/dist/__tests__/audit-ingest.test.d.ts +2 -0
  35. package/dist/__tests__/audit-ingest.test.d.ts.map +1 -0
  36. package/dist/__tests__/audit-ingest.test.js +473 -0
  37. package/dist/__tests__/audit-ingest.test.js.map +1 -0
  38. package/dist/__tests__/audit-memory-ingest.test.d.ts +12 -0
  39. package/dist/__tests__/audit-memory-ingest.test.d.ts.map +1 -0
  40. package/dist/__tests__/audit-memory-ingest.test.js +120 -0
  41. package/dist/__tests__/audit-memory-ingest.test.js.map +1 -0
  42. package/dist/__tests__/audit-property.test.d.ts +2 -0
  43. package/dist/__tests__/audit-property.test.d.ts.map +1 -0
  44. package/dist/__tests__/audit-property.test.js +771 -0
  45. package/dist/__tests__/audit-property.test.js.map +1 -0
  46. package/dist/__tests__/audit-real-fixtures.test.d.ts +2 -0
  47. package/dist/__tests__/audit-real-fixtures.test.d.ts.map +1 -0
  48. package/dist/__tests__/audit-real-fixtures.test.js +132 -0
  49. package/dist/__tests__/audit-real-fixtures.test.js.map +1 -0
  50. package/dist/__tests__/audit-real-live.test.d.ts +2 -0
  51. package/dist/__tests__/audit-real-live.test.d.ts.map +1 -0
  52. package/dist/__tests__/audit-real-live.test.js +107 -0
  53. package/dist/__tests__/audit-real-live.test.js.map +1 -0
  54. package/dist/__tests__/audit-reconstruct.test.d.ts +2 -0
  55. package/dist/__tests__/audit-reconstruct.test.d.ts.map +1 -0
  56. package/dist/__tests__/audit-reconstruct.test.js +287 -0
  57. package/dist/__tests__/audit-reconstruct.test.js.map +1 -0
  58. package/dist/__tests__/audit-report.test.d.ts +2 -0
  59. package/dist/__tests__/audit-report.test.d.ts.map +1 -0
  60. package/dist/__tests__/audit-report.test.js +186 -0
  61. package/dist/__tests__/audit-report.test.js.map +1 -0
  62. package/dist/__tests__/audit-roundtrip.test.d.ts +2 -0
  63. package/dist/__tests__/audit-roundtrip.test.d.ts.map +1 -0
  64. package/dist/__tests__/audit-roundtrip.test.js +368 -0
  65. package/dist/__tests__/audit-roundtrip.test.js.map +1 -0
  66. package/dist/__tests__/audit-temporal.test.d.ts +2 -0
  67. package/dist/__tests__/audit-temporal.test.d.ts.map +1 -0
  68. package/dist/__tests__/audit-temporal.test.js +364 -0
  69. package/dist/__tests__/audit-temporal.test.js.map +1 -0
  70. package/dist/__tests__/audit-tiers.test.d.ts +2 -0
  71. package/dist/__tests__/audit-tiers.test.d.ts.map +1 -0
  72. package/dist/__tests__/audit-tiers.test.js +282 -0
  73. package/dist/__tests__/audit-tiers.test.js.map +1 -0
  74. package/dist/__tests__/audit-witness-real.test.d.ts +19 -0
  75. package/dist/__tests__/audit-witness-real.test.d.ts.map +1 -0
  76. package/dist/__tests__/audit-witness-real.test.js +82 -0
  77. package/dist/__tests__/audit-witness-real.test.js.map +1 -0
  78. package/dist/__tests__/audit-witness.test.d.ts +2 -0
  79. package/dist/__tests__/audit-witness.test.d.ts.map +1 -0
  80. package/dist/__tests__/audit-witness.test.js +308 -0
  81. package/dist/__tests__/audit-witness.test.js.map +1 -0
  82. package/dist/__tests__/chain-hash-anchor.test.d.ts +22 -0
  83. package/dist/__tests__/chain-hash-anchor.test.d.ts.map +1 -0
  84. package/dist/__tests__/chain-hash-anchor.test.js +55 -0
  85. package/dist/__tests__/chain-hash-anchor.test.js.map +1 -0
  86. package/dist/__tests__/declared-proof-fixture.d.ts +66 -0
  87. package/dist/__tests__/declared-proof-fixture.d.ts.map +1 -0
  88. package/dist/__tests__/declared-proof-fixture.js +66 -0
  89. package/dist/__tests__/declared-proof-fixture.js.map +1 -0
  90. package/dist/__tests__/fuse-roundtrip.test.d.ts +2 -0
  91. package/dist/__tests__/fuse-roundtrip.test.d.ts.map +1 -0
  92. package/dist/__tests__/fuse-roundtrip.test.js +172 -0
  93. package/dist/__tests__/fuse-roundtrip.test.js.map +1 -0
  94. package/dist/__tests__/fuse-sdk.test.d.ts +2 -0
  95. package/dist/__tests__/fuse-sdk.test.d.ts.map +1 -0
  96. package/dist/__tests__/fuse-sdk.test.js +117 -0
  97. package/dist/__tests__/fuse-sdk.test.js.map +1 -0
  98. package/dist/__tests__/fuse-vectors.test.d.ts +2 -0
  99. package/dist/__tests__/fuse-vectors.test.d.ts.map +1 -0
  100. package/dist/__tests__/fuse-vectors.test.js +175 -0
  101. package/dist/__tests__/fuse-vectors.test.js.map +1 -0
  102. package/dist/__tests__/fuse-verify.test.d.ts +2 -0
  103. package/dist/__tests__/fuse-verify.test.d.ts.map +1 -0
  104. package/dist/__tests__/fuse-verify.test.js +250 -0
  105. package/dist/__tests__/fuse-verify.test.js.map +1 -0
  106. package/dist/__tests__/mock-proof-fixture.d.ts +15 -0
  107. package/dist/__tests__/mock-proof-fixture.d.ts.map +1 -0
  108. package/dist/__tests__/mock-proof-fixture.js +34 -0
  109. package/dist/__tests__/mock-proof-fixture.js.map +1 -0
  110. package/dist/__tests__/proof-hash-regression.test.js +4 -43
  111. package/dist/__tests__/proof-hash-regression.test.js.map +1 -1
  112. package/dist/__tests__/proof-hash.test.js +3 -21
  113. package/dist/__tests__/proof-hash.test.js.map +1 -1
  114. package/dist/__tests__/proof-integrity.test.d.ts +2 -0
  115. package/dist/__tests__/proof-integrity.test.d.ts.map +1 -0
  116. package/dist/__tests__/proof-integrity.test.js +426 -0
  117. package/dist/__tests__/proof-integrity.test.js.map +1 -0
  118. package/dist/__tests__/realistic-proof-fixture.d.ts +56 -0
  119. package/dist/__tests__/realistic-proof-fixture.d.ts.map +1 -0
  120. package/dist/__tests__/realistic-proof-fixture.js +57 -0
  121. package/dist/__tests__/realistic-proof-fixture.js.map +1 -0
  122. package/dist/__tests__/verifier.test.js +74 -0
  123. package/dist/__tests__/verifier.test.js.map +1 -1
  124. package/dist/fuse-cli.d.ts +3 -0
  125. package/dist/fuse-cli.d.ts.map +1 -0
  126. package/dist/fuse-cli.js +210 -0
  127. package/dist/fuse-cli.js.map +1 -0
  128. package/dist/fuse.d.ts +75 -0
  129. package/dist/fuse.d.ts.map +1 -0
  130. package/dist/fuse.js +273 -0
  131. package/dist/fuse.js.map +1 -0
  132. package/dist/index.d.ts +2 -0
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +4 -0
  135. package/dist/index.js.map +1 -1
  136. package/package.json +19 -9
  137. package/src/__tests__/audit-anchors.test.ts +188 -0
  138. package/src/__tests__/audit-anomalies.test.ts +815 -0
  139. package/src/__tests__/audit-attestation.test.ts +529 -0
  140. package/src/__tests__/audit-bench-smoke.test.ts +108 -0
  141. package/src/__tests__/audit-boundary.test.ts +96 -0
  142. package/src/__tests__/audit-cli.test.ts +288 -0
  143. package/src/__tests__/audit-dependencies.test.ts +141 -0
  144. package/src/__tests__/audit-fixtures.ts +736 -0
  145. package/src/__tests__/audit-ingest.test.ts +575 -0
  146. package/src/__tests__/audit-memory-ingest.test.ts +140 -0
  147. package/src/__tests__/audit-property.test.ts +892 -0
  148. package/src/__tests__/audit-real-fixtures.test.ts +153 -0
  149. package/src/__tests__/audit-real-live.test.ts +128 -0
  150. package/src/__tests__/audit-reconstruct.test.ts +353 -0
  151. package/src/__tests__/audit-report.test.ts +222 -0
  152. package/src/__tests__/audit-roundtrip.test.ts +464 -0
  153. package/src/__tests__/audit-temporal.test.ts +446 -0
  154. package/src/__tests__/audit-tiers.test.ts +343 -0
  155. package/src/__tests__/audit-witness-real.test.ts +92 -0
  156. package/src/__tests__/audit-witness.test.ts +345 -0
  157. package/src/__tests__/chain-hash-anchor.test.ts +67 -0
  158. package/src/__tests__/declared-proof-fixture.ts +65 -0
  159. package/src/__tests__/fuse-roundtrip.test.ts +174 -0
  160. package/src/__tests__/fuse-sdk.test.ts +142 -0
  161. package/src/__tests__/fuse-vectors.test.ts +218 -0
  162. package/src/__tests__/fuse-verify.test.ts +278 -0
  163. package/src/__tests__/mock-proof-fixture.ts +36 -0
  164. package/src/__tests__/proof-hash-regression.test.ts +4 -44
  165. package/src/__tests__/proof-hash.test.ts +3 -22
  166. package/src/__tests__/proof-integrity.test.ts +541 -0
  167. package/src/__tests__/real-fixtures/anchor-after-112762.json +47 -0
  168. package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +47 -0
  169. package/src/__tests__/real-fixtures/live-proof-0.json +42 -0
  170. package/src/__tests__/real-fixtures/live-proof-1.json +43 -0
  171. package/src/__tests__/real-fixtures/live-proof-2.json +43 -0
  172. package/src/__tests__/real-fixtures/successor-112760.json +38 -0
  173. package/src/__tests__/real-fixtures/witness-25525146.json +6 -0
  174. package/src/__tests__/real-fixtures/witness-25525147.json +6 -0
  175. package/src/__tests__/realistic-proof-fixture.ts +57 -0
  176. package/src/__tests__/verifier.test.ts +83 -0
  177. package/src/fuse-cli.ts +187 -0
  178. package/src/fuse.ts +356 -0
  179. package/src/index.ts +6 -0
  180. package/dist/canonical.d.ts +0 -54
  181. package/dist/canonical.d.ts.map +0 -1
  182. package/dist/canonical.js +0 -119
  183. package/dist/canonical.js.map +0 -1
  184. package/dist/proof-hash.d.ts +0 -9
  185. package/dist/proof-hash.d.ts.map +0 -1
  186. package/dist/proof-hash.js +0 -75
  187. package/dist/proof-hash.js.map +0 -1
  188. package/dist/types.d.ts +0 -705
  189. package/dist/types.d.ts.map +0 -1
  190. package/dist/types.js +0 -3
  191. package/dist/types.js.map +0 -1
  192. package/dist/verifier.d.ts +0 -27
  193. package/dist/verifier.d.ts.map +0 -1
  194. package/dist/verifier.js +0 -844
  195. package/dist/verifier.js.map +0 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Option A: a bounded proof bundle (a real export excerpt) audits CLEAN.
3
+ *
4
+ * These are the three real, contiguous proofs from an actual export of the
5
+ * BitGraph whitepaper: the before-anchor (112758) -> the whitepaper commit
6
+ * (112760) -> the after-anchor (112762). They chain intact among themselves;
7
+ * the only "missing" link is 112758's predecessor (112756), which precedes the
8
+ * exported window and is legitimately not included.
9
+ *
10
+ * Before Option A, that leading-edge gap surfaced as a `chain-break-missing`
11
+ * anomaly and set exit code 2, making a perfectly good proof bundle look
12
+ * failed. A validly signed, attested proof only exists by extending the chain
13
+ * (fail-closed construction), so the excerpt boundary is expected, not a
14
+ * defect. The audit now records it as an informational boundary entry point:
15
+ * zero anomalies, chain intact, exit 0. Interior holes remain anomalies (see
16
+ * audit-anomalies "missing middle" / "missing head"), so this does not weaken
17
+ * full-epoch auditing.
18
+ */
19
+
20
+ import { describe, it, before, after } from "node:test";
21
+ import * as assert from "node:assert/strict";
22
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import {
27
+ runAudit,
28
+ buildJsonReport,
29
+ computeExitFlags,
30
+ type AuditResult,
31
+ } from "@mikeargento/bitgraph-audit";
32
+ import { computeProofHash, type BitGraphProof } from "@mikeargento/bitgraph-verify";
33
+
34
+ function safe(value: string): string {
35
+ return value.replace(/[^A-Za-z0-9]/g, "_");
36
+ }
37
+
38
+ describe("audit: bounded export excerpt is clean (Option A boundary)", () => {
39
+ let dir: string;
40
+ let result: AuditResult;
41
+ let proofs: Array<BitGraphProof & { proofHash?: string }>;
42
+
43
+ before(async () => {
44
+ proofs = [];
45
+ for (const name of [
46
+ "anchor-with-ethereum-112758.json",
47
+ "successor-112760.json",
48
+ "anchor-after-112762.json",
49
+ ]) {
50
+ const url = new URL(`../../src/__tests__/real-fixtures/${name}`, import.meta.url);
51
+ proofs.push(JSON.parse(await readFile(fileURLToPath(url), "utf8")));
52
+ }
53
+
54
+ dir = await mkdtemp(join(tmpdir(), "bitgraph-audit-boundary-"));
55
+ for (const proof of proofs) {
56
+ const epochDir = join(dir, "proofs", safe(proof.commit.epochId as string));
57
+ await mkdir(epochDir, { recursive: true });
58
+ const fileName = `${proof.commit.counter}-${safe(computeProofHash(proof))}.json`;
59
+ await writeFile(join(epochDir, fileName), JSON.stringify(proof));
60
+ }
61
+ result = await runAudit(dir);
62
+ });
63
+
64
+ after(async () => {
65
+ if (dir !== undefined) await rm(dir, { recursive: true, force: true });
66
+ });
67
+
68
+ it("reconstructs the excerpt as one intact chain with no anomalies", () => {
69
+ const report = buildJsonReport(result);
70
+ assert.equal(result.ingest.counts.observed, 3);
71
+ assert.equal(result.reconstruction.partitions.length, 1);
72
+ assert.equal(result.reconstruction.partitions[0]!.components.length, 1);
73
+ assert.equal(result.anomalies.anomalies.length, 0);
74
+ assert.equal(result.anomalies.divergences.length, 0);
75
+ assert.equal(report.summary.chainIntact, true);
76
+ assert.equal(report.summary.partitionsIntact, 1);
77
+ });
78
+
79
+ it("records exactly one boundary entry point: the excerpt's earliest proof (112758)", () => {
80
+ const report = buildJsonReport(result);
81
+ assert.equal(report.summary.boundaryEntryPoints, 1);
82
+ assert.equal(report.boundaryEntryPoints.length, 1);
83
+ const boundary = report.boundaryEntryPoints[0]!;
84
+ const earliest = proofs.find((p) => p.commit.counter === "112758")!;
85
+ assert.equal(boundary.proofHash, computeProofHash(earliest));
86
+ assert.equal(boundary.prevB64, earliest.commit.prevB64);
87
+ });
88
+
89
+ it("exits clean (code 0): a bounded excerpt is not a failure", () => {
90
+ assert.deepEqual(computeExitFlags(result), {
91
+ verificationFailures: false,
92
+ chainAnomaliesOrDivergences: false,
93
+ code: 0,
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,288 @@
1
+ // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
2
+
3
+ /**
4
+ * Tests for the bitgraph-audit CLI: spawns the built packages/audit
5
+ * dist/cli.js with node against temp-dir bundles and asserts the written
6
+ * report files, the bit-flag exit codes (0 clean, 1 verification
7
+ * failures including unsupported-version, 2 chain anomalies or
8
+ * divergences, 3 both, 64 usage error), format selection, trust-policy
9
+ * key validation, and the --help exit-code documentation.
10
+ */
11
+
12
+ import { describe, it, before, after } from "node:test";
13
+ import * as assert from "node:assert/strict";
14
+ import { spawnSync } from "node:child_process";
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { rm, writeFile } from "node:fs/promises";
17
+ import { join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import {
20
+ healthyPairs,
21
+ makeAnchorProof,
22
+ makeCounterChain,
23
+ makeEthereumHeader,
24
+ makeStandardAuditBundle,
25
+ makeTempDir,
26
+ proofJson,
27
+ signBody,
28
+ utf8,
29
+ witnessJson,
30
+ writeBundleDir,
31
+ b64,
32
+ type StandardAuditBundle,
33
+ } from "./audit-fixtures.js";
34
+ import { sha256 } from "@noble/hashes/sha256";
35
+ import { keccak_256 } from "@noble/hashes/sha3";
36
+ import { computeProofHash } from "@mikeargento/bitgraph-verify";
37
+ import type { BitGraphProof } from "@mikeargento/bitgraph-verify";
38
+
39
+ const CLI_PATH = fileURLToPath(
40
+ new URL("../../packages/audit/dist/cli.js", import.meta.url)
41
+ );
42
+
43
+ function runCli(args: string[]): { status: number; stdout: string; stderr: string } {
44
+ const spawned = spawnSync(process.execPath, [CLI_PATH, ...args], { encoding: "utf8" });
45
+ return {
46
+ status: spawned.status ?? -1,
47
+ stdout: spawned.stdout ?? "",
48
+ stderr: spawned.stderr ?? "",
49
+ };
50
+ }
51
+
52
+ describe("bitgraph-audit CLI", () => {
53
+ const tempDirs: string[] = [];
54
+ let mixed: StandardAuditBundle;
55
+ let cleanDir: string;
56
+ let rejectOnlyDir: string;
57
+ let forkOnlyDir: string;
58
+
59
+ before(async () => {
60
+ // Mixed bundle: gap + fork (bit 2) and an occ/1 reject (bit 1).
61
+ mixed = await makeStandardAuditBundle();
62
+ tempDirs.push(mixed.dir);
63
+
64
+ // Clean bundle: healthy chain with every artifact present.
65
+ cleanDir = await makeTempDir("bitgraph-audit-cli-clean-");
66
+ tempDirs.push(cleanDir);
67
+ const clean = await makeCounterChain({
68
+ epochId: "epoch-cli-clean",
69
+ pairs: healthyPairs(2),
70
+ chainId: "bitgraph:main",
71
+ payloadPrefix: "cli-clean",
72
+ });
73
+ await writeBundleDir(cleanDir, {
74
+ "proofs/a.json": proofJson(clean.proofs[0]!.proof),
75
+ "proofs/b.json": proofJson(clean.proofs[1]!.proof),
76
+ "artifacts/a.bin": clean.proofs[0]!.bytes,
77
+ "artifacts/b.bin": clean.proofs[1]!.bytes,
78
+ });
79
+
80
+ // Reject-only bundle: a single occ/1 proof-shaped file.
81
+ rejectOnlyDir = await makeTempDir("bitgraph-audit-cli-reject-");
82
+ tempDirs.push(rejectOnlyDir);
83
+ await writeBundleDir(rejectOnlyDir, {
84
+ "old.json": JSON.stringify({
85
+ version: "occ/1",
86
+ artifact: { hashAlg: "sha256", digestB64: "b2NjLWxlZ2FjeQ==" },
87
+ commit: { nonceB64: "b2NjLW5vbmNl" },
88
+ signer: { publicKeyB64: "b2NjLWtleQ==", signatureB64: "b2NjLXNpZw==" },
89
+ }),
90
+ });
91
+
92
+ // Fork-only bundle: valid proofs, one predecessor consumed twice.
93
+ forkOnlyDir = await makeTempDir("bitgraph-audit-cli-fork-");
94
+ tempDirs.push(forkOnlyDir);
95
+ const base = await makeCounterChain({
96
+ epochId: "epoch-cli-fork",
97
+ pairs: healthyPairs(2),
98
+ chainId: "bitgraph:main",
99
+ payloadPrefix: "cli-fork",
100
+ });
101
+ const tail = base.proofs[1]!;
102
+ const forkChild = async (slot: string, commit: string, payload: string) => {
103
+ const bytes = utf8(payload);
104
+ const commitBody: BitGraphProof["commit"] = {
105
+ nonceB64: b64(crypto.getRandomValues(new Uint8Array(16))),
106
+ counter: commit,
107
+ slotCounter: slot,
108
+ prevB64: tail.chainHash,
109
+ epochId: "epoch-cli-fork",
110
+ };
111
+ (commitBody as unknown as Record<string, unknown>)["chainId"] = "bitgraph:main";
112
+ const proof = await signBody(
113
+ base.key,
114
+ { hashAlg: "sha256", digestB64: b64(sha256(bytes)) },
115
+ commitBody,
116
+ "test-measurement-chain"
117
+ );
118
+ return { proof, proofHash: computeProofHash(proof) };
119
+ };
120
+ const childA = await forkChild("5", "6", "cli-fork-a");
121
+ const childB = await forkChild("7", "8", "cli-fork-b");
122
+ await writeBundleDir(forkOnlyDir, {
123
+ "proofs/base-0.json": proofJson(base.proofs[0]!.proof),
124
+ "proofs/base-1.json": proofJson(tail.proof),
125
+ "proofs/fork-a.json": proofJson(childA.proof),
126
+ "proofs/fork-b.json": proofJson(childB.proof),
127
+ });
128
+ });
129
+
130
+ after(async () => {
131
+ for (const dir of tempDirs) await rm(dir, { recursive: true, force: true });
132
+ });
133
+
134
+ it("exits 3 on the mixed bundle and writes both report files", async () => {
135
+ const outDir = await makeTempDir("bitgraph-audit-cli-out-mixed-");
136
+ tempDirs.push(outDir);
137
+ const run = runCli([mixed.dir, "--out", outDir]);
138
+ assert.equal(run.status, 3, run.stderr);
139
+
140
+ const jsonPath = join(outDir, "audit-report.json");
141
+ const mdPath = join(outDir, "audit-report.md");
142
+ assert.ok(existsSync(jsonPath), "audit-report.json written");
143
+ assert.ok(existsSync(mdPath), "audit-report.md written");
144
+
145
+ // stdout: the short completion line names the files and the exit meaning.
146
+ assert.ok(run.stdout.includes("audit-report.json"));
147
+ assert.ok(run.stdout.includes("audit-report.md"));
148
+ assert.ok(run.stdout.includes("exit 3"));
149
+ assert.ok(run.stdout.includes("verification failures"));
150
+ assert.ok(run.stdout.includes("chain anomalies"));
151
+
152
+ // The written JSON is the real report.
153
+ const written = JSON.parse(readFileSync(jsonPath, "utf8")) as {
154
+ reportSchemaVersion: string;
155
+ summary: { exit: { code: number } };
156
+ };
157
+ assert.equal(written.reportSchemaVersion, "bitgraph-audit-report/2");
158
+ assert.equal(written.summary.exit.code, 3);
159
+ // No em dashes in either written file.
160
+ assert.ok(!readFileSync(jsonPath, "utf8").includes("—"));
161
+ assert.ok(!readFileSync(mdPath, "utf8").includes("—"));
162
+ });
163
+
164
+ it("exits 0 on a clean chain-only bundle", async () => {
165
+ const outDir = await makeTempDir("bitgraph-audit-cli-out-clean-");
166
+ tempDirs.push(outDir);
167
+ const run = runCli([cleanDir, "--out", outDir]);
168
+ assert.equal(run.status, 0, run.stderr);
169
+ assert.ok(run.stdout.includes("exit 0"));
170
+ assert.ok(run.stdout.includes("clean"));
171
+ assert.ok(existsSync(join(outDir, "audit-report.json")));
172
+ assert.ok(existsSync(join(outDir, "audit-report.md")));
173
+ });
174
+
175
+ it("exits 1 on a reject-only bundle (unsupported-version counts as verification failure)", async () => {
176
+ const outDir = await makeTempDir("bitgraph-audit-cli-out-reject-");
177
+ tempDirs.push(outDir);
178
+ const run = runCli([rejectOnlyDir, "--out", outDir]);
179
+ assert.equal(run.status, 1, run.stderr);
180
+ assert.ok(run.stdout.includes("exit 1"));
181
+ });
182
+
183
+ it("exits 2 on a fork-only bundle (divergence between valid proofs)", async () => {
184
+ const outDir = await makeTempDir("bitgraph-audit-cli-out-fork-");
185
+ tempDirs.push(outDir);
186
+ const run = runCli([forkOnlyDir, "--out", outDir]);
187
+ assert.equal(run.status, 2, run.stderr);
188
+ assert.ok(run.stdout.includes("exit 2"));
189
+ });
190
+
191
+ it("exits 2 on a bundle whose anchor witness fails offline verification", async () => {
192
+ const witnessDir = await makeTempDir("bitgraph-audit-cli-witness-");
193
+ tempDirs.push(witnessDir);
194
+ const blockNumber = 555000;
195
+ // The anchor commits the hash of the correct header; the witness carries
196
+ // a tampered header (different timestamp, so a different keccak hash)
197
+ // while claiming the correct block hash, so it still matches the anchor
198
+ // and fails loudly at the hash step instead of disappearing as unmatched.
199
+ const correct = makeEthereumHeader({ blockNumber, timestamp: 1_700_000_000 });
200
+ const correctHash = `0x${Buffer.from(keccak_256(correct.headerBytes)).toString("hex")}`;
201
+ const tampered = makeEthereumHeader({ blockNumber, timestamp: 1_699_999_999 });
202
+ const anchor = await makeAnchorProof({
203
+ blockHash: correctHash,
204
+ blockNumber,
205
+ epochId: "epoch-cli-witness",
206
+ counter: "2",
207
+ slotCounter: "1",
208
+ });
209
+ await writeBundleDir(witnessDir, {
210
+ "proofs/anchor.json": proofJson(anchor.proof),
211
+ "witnesses/block.json": witnessJson({
212
+ headerRlpHex: tampered.headerRlpHex,
213
+ blockNumber,
214
+ blockHash: correctHash,
215
+ }),
216
+ });
217
+
218
+ const outDir = await makeTempDir("bitgraph-audit-cli-out-witness-");
219
+ tempDirs.push(outDir);
220
+ const run = runCli([witnessDir, "--out", outDir]);
221
+ // The anchor itself verifies (no bytes: artifact-unavailable, not a
222
+ // failure), so bit 1 stays clear; the witness verification failure sets
223
+ // bit 2. Before the fix this bundle exited 0 "clean".
224
+ assert.equal(run.status, 2, run.stderr);
225
+ assert.ok(run.stdout.includes("exit 2"));
226
+ assert.ok(run.stdout.includes("anchor witness verification failures"));
227
+ const report = JSON.parse(readFileSync(join(outDir, "audit-report.json"), "utf8")) as {
228
+ summary: { exit: { code: number }; anomalyCountsByCode: Record<string, number> };
229
+ };
230
+ assert.equal(report.summary.exit.code, 2);
231
+ assert.ok((report.summary.anomalyCountsByCode["witness-hash-mismatch"] ?? 0) >= 1);
232
+ });
233
+
234
+ it("exits 1 when a supplied policy makes bytes-free proofs fail (requireSlot)", async () => {
235
+ const outDir = await makeTempDir("bitgraph-audit-cli-out-policy-");
236
+ tempDirs.push(outDir);
237
+ const policyPath = join(outDir, "policy.json");
238
+ await writeFile(policyPath, JSON.stringify({ requireSlot: true }), "utf8");
239
+ // Fork-only proofs carry no slotAllocation record, so requireSlot
240
+ // fails them; the divergence keeps bit 2 set as well.
241
+ const run = runCli([forkOnlyDir, "--out", outDir, "--trust-policy", policyPath]);
242
+ assert.equal(run.status, 3, run.stderr);
243
+ });
244
+
245
+ it("writes only JSON with --format json", async () => {
246
+ const outDir = await makeTempDir("bitgraph-audit-cli-out-json-");
247
+ tempDirs.push(outDir);
248
+ const run = runCli([cleanDir, "--out", outDir, "--format", "json"]);
249
+ assert.equal(run.status, 0, run.stderr);
250
+ assert.ok(existsSync(join(outDir, "audit-report.json")));
251
+ assert.ok(!existsSync(join(outDir, "audit-report.md")));
252
+ assert.ok(!run.stdout.includes("audit-report.md"));
253
+ });
254
+
255
+ it("rejects an unknown trust-policy key with usage and the valid field list", async () => {
256
+ const outDir = await makeTempDir("bitgraph-audit-cli-out-badpolicy-");
257
+ tempDirs.push(outDir);
258
+ const policyPath = join(outDir, "bad-policy.json");
259
+ await writeFile(policyPath, JSON.stringify({ requireBytes: true }), "utf8");
260
+ const run = runCli([cleanDir, "--out", outDir, "--trust-policy", policyPath]);
261
+ assert.equal(run.status, 64);
262
+ assert.ok(run.stderr.includes("requireBytes"), "names the unknown key");
263
+ assert.ok(run.stderr.includes("allowedMeasurements"), "lists the valid keys");
264
+ assert.ok(run.stderr.includes("requireSlot"), "lists the valid keys");
265
+ assert.ok(run.stderr.includes("Usage:"), "prints usage");
266
+ assert.ok(!existsSync(join(outDir, "audit-report.json")), "no report on usage error");
267
+ });
268
+
269
+ it("documents the exit-code semantics in --help", () => {
270
+ const run = runCli(["--help"]);
271
+ assert.equal(run.status, 0);
272
+ assert.ok(run.stdout.includes("Exit codes (bit flags):"));
273
+ for (const marker of ["0 ", "1 ", "2 ", "3 ", "64"]) {
274
+ assert.ok(run.stdout.includes(` ${marker}`), `documents exit code ${marker.trim()}`);
275
+ }
276
+ assert.ok(run.stdout.includes("unsupported version"));
277
+ assert.ok(run.stdout.includes("NOT a failure"));
278
+ assert.ok(run.stdout.includes("Attestation validation results"));
279
+ assert.ok(!run.stdout.includes("—"), "help text contains no em dashes");
280
+ });
281
+
282
+ it("errors with usage when no bundle path is given", () => {
283
+ const run = runCli([]);
284
+ assert.equal(run.status, 64);
285
+ assert.ok(run.stderr.includes("missing bundle path"));
286
+ assert.ok(run.stderr.includes("Usage:"));
287
+ });
288
+ });
@@ -0,0 +1,141 @@
1
+ // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
2
+
3
+ /**
4
+ * Dependency audit for @mikeargento/bitgraph-audit (Phase 4e).
5
+ *
6
+ * The audit package promises zero runtime network access. This suite pins
7
+ * that promise mechanically:
8
+ *
9
+ * 1. The declared runtime dependency closure is exactly
10
+ * @mikeargento/bitgraph-verify plus @noble/hashes, with
11
+ * @noble/ed25519 arriving only transitively through verify. Nothing
12
+ * else, and no peer or optional dependencies that could widen it.
13
+ * 2. An fs walk of the shipped module graphs (packages/audit/dist and
14
+ * packages/verify/dist, which is inside the runtime closure) asserts
15
+ * no compiled file references node:http, node:https, node:net,
16
+ * node:dgram, node:tls, XMLHttpRequest, WebSocket, a bare
17
+ * http/https/net/tls/dgram import, or a global fetch call.
18
+ *
19
+ * The @noble packages were audited by hand and recorded in DECISIONS.md:
20
+ * pure crypto, no network APIs. (@noble/hashes sha3-addons defines a hash
21
+ * CLASS METHOD named fetch for XOF output; it is not the global fetch and
22
+ * the audit package does not import sha3-addons.)
23
+ */
24
+
25
+ import { describe, it } from "node:test";
26
+ import * as assert from "node:assert/strict";
27
+ import { readFile, readdir } from "node:fs/promises";
28
+ import { fileURLToPath } from "node:url";
29
+ import { join } from "node:path";
30
+
31
+ // Compiled location: <repo>/dist/__tests__/audit-dependencies.test.js
32
+ const repoRoot = fileURLToPath(new URL("../..", import.meta.url));
33
+
34
+ async function walkJsFiles(dir: string): Promise<string[]> {
35
+ const out: string[] = [];
36
+ const entries = await readdir(dir, { withFileTypes: true });
37
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
38
+ for (const entry of entries) {
39
+ const abs = join(dir, entry.name);
40
+ if (entry.isDirectory()) {
41
+ out.push(...(await walkJsFiles(abs)));
42
+ } else if (entry.isFile() && entry.name.endsWith(".js")) {
43
+ out.push(abs);
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * Forbidden references. Substring checks for the node: specifiers and the
51
+ * browser network globals (these strings have no legitimate reason to
52
+ * appear anywhere in the compiled output, comments included), plus
53
+ * pattern checks for bare-specifier network imports and global fetch
54
+ * calls.
55
+ */
56
+ const FORBIDDEN_SUBSTRINGS = [
57
+ "node:http",
58
+ "node:https",
59
+ "node:net",
60
+ "node:dgram",
61
+ "node:tls",
62
+ "XMLHttpRequest",
63
+ "WebSocket",
64
+ ] as const;
65
+
66
+ const FORBIDDEN_PATTERNS: ReadonlyArray<[name: string, pattern: RegExp]> = [
67
+ ["bare network import", /(?:from\s*|require\s*\(\s*|import\s*\(\s*)["'](?:http|https|net|tls|dgram)["']/],
68
+ ["global fetch call", /\bfetch\s*\(/],
69
+ ];
70
+
71
+ async function assertGraphClean(distDir: string): Promise<number> {
72
+ const files = await walkJsFiles(distDir);
73
+ assert.ok(files.length > 0, `compiled module graph exists at ${distDir} (run the build first)`);
74
+ for (const file of files) {
75
+ const source = await readFile(file, "utf8");
76
+ for (const needle of FORBIDDEN_SUBSTRINGS) {
77
+ assert.ok(!source.includes(needle), `${file} must not reference ${needle}`);
78
+ }
79
+ for (const [name, pattern] of FORBIDDEN_PATTERNS) {
80
+ assert.ok(!pattern.test(source), `${file} must not contain a ${name}`);
81
+ }
82
+ }
83
+ return files.length;
84
+ }
85
+
86
+ describe("audit package dependency audit: zero network surface", () => {
87
+ it("declares exactly the allowed runtime dependency closure", async () => {
88
+ const auditPkg = JSON.parse(
89
+ await readFile(join(repoRoot, "packages", "audit", "package.json"), "utf8")
90
+ ) as Record<string, Record<string, string> | undefined>;
91
+ const verifyPkg = JSON.parse(
92
+ await readFile(join(repoRoot, "packages", "verify", "package.json"), "utf8")
93
+ ) as Record<string, Record<string, string> | undefined>;
94
+
95
+ assert.deepEqual(
96
+ Object.keys(auditPkg["dependencies"] ?? {}).sort(),
97
+ ["@mikeargento/bitgraph-verify", "@noble/hashes"],
98
+ "audit runtime dependencies are exactly verify + @noble/hashes"
99
+ );
100
+ assert.deepEqual(
101
+ Object.keys(verifyPkg["dependencies"] ?? {}).sort(),
102
+ ["@noble/ed25519", "@noble/hashes"],
103
+ "verify (the only workspace dependency) brings exactly the @noble crypto pair"
104
+ );
105
+ // No side doors that could widen the closure at install time.
106
+ for (const [pkgName, pkg] of [
107
+ ["audit", auditPkg],
108
+ ["verify", verifyPkg],
109
+ ] as const) {
110
+ assert.equal(pkg["peerDependencies"], undefined, `${pkgName} has no peerDependencies`);
111
+ assert.equal(pkg["optionalDependencies"], undefined, `${pkgName} has no optionalDependencies`);
112
+ assert.equal(pkg["bundledDependencies"], undefined, `${pkgName} has no bundledDependencies`);
113
+ }
114
+ });
115
+
116
+ it("packages/audit/dist references no network API", async () => {
117
+ const count = await assertGraphClean(join(repoRoot, "packages", "audit", "dist"));
118
+ assert.ok(count >= 15, `walked the full audit module graph (${count} files)`);
119
+ });
120
+
121
+ it("packages/verify/dist (runtime closure member) references no network API", async () => {
122
+ const count = await assertGraphClean(join(repoRoot, "packages", "verify", "dist"));
123
+ assert.ok(count >= 4, `walked the full verify module graph (${count} files)`);
124
+ });
125
+
126
+ it("README ships resolvable doc links (absolute GitHub URLs, never repo-relative)", async () => {
127
+ // README.md is in the package "files" allowlist but docs/ is not, so
128
+ // repo-relative doc links would be dead on npmjs.com. They must be
129
+ // absolute GitHub URLs.
130
+ const readme = await readFile(
131
+ join(repoRoot, "packages", "audit", "README.md"),
132
+ "utf8"
133
+ );
134
+ const base = "https://github.com/mikeargento/bitgraph/blob/main/docs";
135
+ assert.ok(readme.includes(`${base}/BUNDLE-FORMAT.md`), "BUNDLE-FORMAT.md link is absolute");
136
+ assert.ok(readme.includes(`${base}/HOW-TO-AUDIT.md`), "HOW-TO-AUDIT.md link is absolute");
137
+ // No markdown link target resolves to a repo-relative docs path.
138
+ assert.ok(!/\]\(docs\//.test(readme), "no repo-relative docs/ link targets");
139
+ assert.ok(!/\]\(\.{1,2}\/.*docs\//.test(readme), "no relative docs/ link targets");
140
+ });
141
+ });