@mikeargento/bitgraph 1.9.0 → 1.9.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.
Files changed (128) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +58 -35
  3. package/dist/__tests__/audit-anchors.test.js +1 -1
  4. package/dist/__tests__/audit-anchors.test.js.map +1 -1
  5. package/dist/__tests__/audit-anomalies.test.js +1 -1
  6. package/dist/__tests__/audit-anomalies.test.js.map +1 -1
  7. package/dist/__tests__/audit-attestation.test.js +1 -1
  8. package/dist/__tests__/audit-attestation.test.js.map +1 -1
  9. package/dist/__tests__/audit-bench-smoke.test.js +1 -1
  10. package/dist/__tests__/audit-bench-smoke.test.js.map +1 -1
  11. package/dist/__tests__/audit-cli.test.js +1 -1
  12. package/dist/__tests__/audit-cli.test.js.map +1 -1
  13. package/dist/__tests__/audit-dependencies.test.js +1 -1
  14. package/dist/__tests__/audit-dependencies.test.js.map +1 -1
  15. package/dist/__tests__/audit-fixtures.js +1 -1
  16. package/dist/__tests__/audit-fixtures.js.map +1 -1
  17. package/dist/__tests__/audit-ingest.test.js +1 -1
  18. package/dist/__tests__/audit-ingest.test.js.map +1 -1
  19. package/dist/__tests__/audit-property.test.js +1 -1
  20. package/dist/__tests__/audit-property.test.js.map +1 -1
  21. package/dist/__tests__/audit-real-fixtures.test.js +1 -1
  22. package/dist/__tests__/audit-real-fixtures.test.js.map +1 -1
  23. package/dist/__tests__/audit-real-live.test.js +1 -1
  24. package/dist/__tests__/audit-real-live.test.js.map +1 -1
  25. package/dist/__tests__/audit-reconstruct.test.js +1 -1
  26. package/dist/__tests__/audit-reconstruct.test.js.map +1 -1
  27. package/dist/__tests__/audit-report.test.js +1 -1
  28. package/dist/__tests__/audit-report.test.js.map +1 -1
  29. package/dist/__tests__/audit-roundtrip.test.js +1 -1
  30. package/dist/__tests__/audit-roundtrip.test.js.map +1 -1
  31. package/dist/__tests__/audit-temporal.test.js +1 -1
  32. package/dist/__tests__/audit-temporal.test.js.map +1 -1
  33. package/dist/__tests__/audit-tiers.test.js +1 -1
  34. package/dist/__tests__/audit-tiers.test.js.map +1 -1
  35. package/dist/__tests__/audit-witness.test.js +1 -1
  36. package/dist/__tests__/audit-witness.test.js.map +1 -1
  37. package/dist/__tests__/canonical.test.js +1 -1
  38. package/dist/__tests__/canonical.test.js.map +1 -1
  39. package/dist/__tests__/commit-service.integration.test.js +1 -1
  40. package/dist/__tests__/commit-service.integration.test.js.map +1 -1
  41. package/dist/__tests__/constructor.test.js +1 -1
  42. package/dist/__tests__/constructor.test.js.map +1 -1
  43. package/dist/__tests__/fuse-inline.test.d.ts +2 -0
  44. package/dist/__tests__/fuse-inline.test.d.ts.map +1 -0
  45. package/dist/__tests__/fuse-inline.test.js +181 -0
  46. package/dist/__tests__/fuse-inline.test.js.map +1 -0
  47. package/dist/__tests__/fuse-placement.test.js +1 -1
  48. package/dist/__tests__/fuse-placement.test.js.map +1 -1
  49. package/dist/__tests__/fuse-roundtrip.test.js +1 -1
  50. package/dist/__tests__/fuse-roundtrip.test.js.map +1 -1
  51. package/dist/__tests__/fuse-sdk.test.js +1 -1
  52. package/dist/__tests__/fuse-sdk.test.js.map +1 -1
  53. package/dist/__tests__/fuse-set-sdk.test.js +1 -1
  54. package/dist/__tests__/fuse-set-sdk.test.js.map +1 -1
  55. package/dist/__tests__/fuse-set.test.js +24 -3
  56. package/dist/__tests__/fuse-set.test.js.map +1 -1
  57. package/dist/__tests__/fuse-set2.test.js +1 -1
  58. package/dist/__tests__/fuse-set2.test.js.map +1 -1
  59. package/dist/__tests__/fuse-vectors.test.js +1 -1
  60. package/dist/__tests__/fuse-vectors.test.js.map +1 -1
  61. package/dist/__tests__/fuse-verify.test.js +1 -1
  62. package/dist/__tests__/fuse-verify.test.js.map +1 -1
  63. package/dist/__tests__/hello-world.test.js +1 -1
  64. package/dist/__tests__/hello-world.test.js.map +1 -1
  65. package/dist/__tests__/policy-enforcement.test.js +1 -1
  66. package/dist/__tests__/policy-enforcement.test.js.map +1 -1
  67. package/dist/__tests__/proof-integrity.test.js +30 -2
  68. package/dist/__tests__/proof-integrity.test.js.map +1 -1
  69. package/dist/__tests__/run.test.d.ts +2 -0
  70. package/dist/__tests__/run.test.d.ts.map +1 -0
  71. package/dist/__tests__/run.test.js +178 -0
  72. package/dist/__tests__/run.test.js.map +1 -0
  73. package/dist/__tests__/verifier.test.js +1 -1
  74. package/dist/__tests__/verifier.test.js.map +1 -1
  75. package/dist/constructor.js +1 -1
  76. package/dist/constructor.js.map +1 -1
  77. package/dist/fuse-cli.js +1 -1
  78. package/dist/fuse-cli.js.map +1 -1
  79. package/dist/fuse.js +1 -1
  80. package/dist/fuse.js.map +1 -1
  81. package/dist/host.js +1 -1
  82. package/dist/host.js.map +1 -1
  83. package/dist/index.d.ts +2 -1
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +2 -2
  86. package/dist/index.js.map +1 -1
  87. package/dist/policy.js +1 -1
  88. package/dist/policy.js.map +1 -1
  89. package/package.json +16 -4
  90. package/src/__tests__/audit-anchors.test.ts +1 -1
  91. package/src/__tests__/audit-anomalies.test.ts +1 -1
  92. package/src/__tests__/audit-attestation.test.ts +1 -1
  93. package/src/__tests__/audit-bench-smoke.test.ts +1 -1
  94. package/src/__tests__/audit-cli.test.ts +1 -1
  95. package/src/__tests__/audit-dependencies.test.ts +1 -1
  96. package/src/__tests__/audit-fixtures.ts +1 -1
  97. package/src/__tests__/audit-ingest.test.ts +1 -1
  98. package/src/__tests__/audit-property.test.ts +1 -1
  99. package/src/__tests__/audit-real-fixtures.test.ts +1 -1
  100. package/src/__tests__/audit-real-live.test.ts +1 -1
  101. package/src/__tests__/audit-reconstruct.test.ts +1 -1
  102. package/src/__tests__/audit-report.test.ts +1 -1
  103. package/src/__tests__/audit-roundtrip.test.ts +1 -1
  104. package/src/__tests__/audit-temporal.test.ts +1 -1
  105. package/src/__tests__/audit-tiers.test.ts +1 -1
  106. package/src/__tests__/audit-witness.test.ts +1 -1
  107. package/src/__tests__/canonical.test.ts +1 -1
  108. package/src/__tests__/commit-service.integration.test.ts +1 -1
  109. package/src/__tests__/constructor.test.ts +1 -1
  110. package/src/__tests__/fuse-inline.test.ts +211 -0
  111. package/src/__tests__/fuse-placement.test.ts +1 -1
  112. package/src/__tests__/fuse-roundtrip.test.ts +1 -1
  113. package/src/__tests__/fuse-sdk.test.ts +1 -1
  114. package/src/__tests__/fuse-set-sdk.test.ts +1 -1
  115. package/src/__tests__/fuse-set.test.ts +25 -3
  116. package/src/__tests__/fuse-set2.test.ts +1 -1
  117. package/src/__tests__/fuse-vectors.test.ts +1 -1
  118. package/src/__tests__/fuse-verify.test.ts +1 -1
  119. package/src/__tests__/hello-world.test.ts +1 -1
  120. package/src/__tests__/policy-enforcement.test.ts +1 -1
  121. package/src/__tests__/proof-integrity.test.ts +36 -1
  122. package/src/__tests__/verifier.test.ts +1 -1
  123. package/src/constructor.ts +1 -1
  124. package/src/fuse-cli.ts +1 -1
  125. package/src/fuse.ts +1 -1
  126. package/src/host.ts +1 -1
  127. package/src/index.ts +3 -2
  128. package/src/policy.ts +1 -1
@@ -0,0 +1,211 @@
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
+
3
+ /**
4
+ * The marker's title in its second vocabulary: an ENCODING rather than a
5
+ * placement, for an artifact MADE with the commitment inside it.
6
+ *
7
+ * A placement is a recipe, so a holder of the original rebuilds the committed
8
+ * artifact byte for byte. An encoding says there is no recipe and no original:
9
+ * the commitment was in the thing while it was being made (a prompt, a build
10
+ * id, a commit message), so the only thing to do is look for it. One marker
11
+ * answers both, which is why this file tests verifyFuse and not a second
12
+ * function. It was briefly a second profile, bitgraph-run/1, on 2026-09-07.
13
+ *
14
+ * The positive case is real: the canonical inline artifact minted on
15
+ * production, enclave v8, slot 2072 consumed at commit 2075. Categories that
16
+ * need a key under this test's control mint their own proofs, because every one
17
+ * of them is reached by changing something the enclave signed.
18
+ */
19
+
20
+ import { describe, test } from "node:test";
21
+ import * as assert from "node:assert/strict";
22
+ import { readFileSync } from "node:fs";
23
+ import { fileURLToPath } from "node:url";
24
+ import { sha256 } from "@noble/hashes/sha256";
25
+ import { getPublicKeyAsync, signAsync, utils } from "@noble/ed25519";
26
+ import {
27
+ verifyFuse,
28
+ inlineAttribution,
29
+ isCarryEncoding,
30
+ findCommitment,
31
+ computeSlotCommitment,
32
+ canonicalize,
33
+ bytesToBase64,
34
+ ENCODING_BASE64URL,
35
+ } from "@mikeargento/bitgraph-verify";
36
+ import type { BitGraphProof } from "@mikeargento/bitgraph-verify";
37
+
38
+ const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
39
+ const b64 = (b: Uint8Array) => bytesToBase64(b);
40
+ const b64u = (b: Uint8Array) => b64(b).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
41
+ const utf8 = (s: string) => new TextEncoder().encode(s);
42
+
43
+ const liveProof = JSON.parse(readFileSync(FIX + "inline.proof.json", "utf8")) as BitGraphProof;
44
+ const liveBytes = new Uint8Array(readFileSync(FIX + "inline.artifact.txt"));
45
+
46
+ /** Mint a proof the way the enclave does, so the signature covers the marker. */
47
+ async function mint(
48
+ makeBytes: (commitment: string) => Uint8Array,
49
+ opts: { title?: string; message?: string } = {},
50
+ ) {
51
+ const seed = utils.randomPrivateKey();
52
+ const publicKeyB64 = b64(await getPublicKeyAsync(seed));
53
+ const slotBody = {
54
+ version: "bitgraph/slot/1" as const,
55
+ nonceB64: b64(new Uint8Array(32).fill(7)),
56
+ counter: "41",
57
+ epochId: "inline-test-epoch",
58
+ publicKeyB64,
59
+ };
60
+ const slotAllocation = { ...slotBody, signatureB64: b64(await signAsync(canonicalize(slotBody), seed)) };
61
+ const commitment = b64u(computeSlotCommitment(slotAllocation));
62
+ const bytes = makeBytes(commitment);
63
+ const digestB64 = b64(sha256(bytes));
64
+ const attribution: Record<string, string> = { name: "bitgraph-fuse/1", title: opts.title ?? ENCODING_BASE64URL };
65
+ if (opts.message !== undefined) attribution["message"] = opts.message;
66
+ const commit = {
67
+ nonceB64: slotBody.nonceB64,
68
+ counter: "42",
69
+ slotCounter: "41",
70
+ slotHashB64: b64(sha256(canonicalize(slotBody))),
71
+ epochId: "inline-test-epoch",
72
+ };
73
+ const signedBody = {
74
+ version: "bitgraph/1" as const,
75
+ artifact: { hashAlg: "sha256" as const, digestB64 },
76
+ commit,
77
+ publicKeyB64,
78
+ enforcement: "measured-tee" as const,
79
+ measurement: "inline-test-measurement",
80
+ attribution,
81
+ };
82
+ const proof = {
83
+ version: "bitgraph/1",
84
+ artifact: signedBody.artifact,
85
+ commit,
86
+ signer: { publicKeyB64, signatureB64: b64(await signAsync(canonicalize(signedBody), seed)) },
87
+ environment: { enforcement: "measured-tee", measurement: "inline-test-measurement" },
88
+ slotAllocation,
89
+ attribution,
90
+ } as unknown as BitGraphProof;
91
+ return { proof, bytes, commitment };
92
+ }
93
+
94
+ describe("carry encodings", () => {
95
+ test("base64url is registered; a placement id and nonsense are not", () => {
96
+ assert.equal(isCarryEncoding(ENCODING_BASE64URL), true);
97
+ for (const id of ["trailer/1", "container/2", "set/1", "hex/1", "", null, undefined]) {
98
+ assert.equal(isCarryEncoding(id as string), false, `${String(id)} should not be an encoding`);
99
+ }
100
+ });
101
+
102
+ test("inlineAttribution declares the encoding and no origin, because there is none", () => {
103
+ const a = inlineAttribution();
104
+ assert.deepEqual(a, { name: "bitgraph-fuse/1", title: ENCODING_BASE64URL });
105
+ assert.equal("message" in a, false, "a 32-byte message would be read as an origin digest");
106
+ assert.deepEqual(a, liveProof.attribution);
107
+ });
108
+
109
+ test("findCommitment locates every occurrence, and none for an unregistered encoding", () => {
110
+ const c = new Uint8Array(32).fill(3);
111
+ const text = b64u(c);
112
+ assert.deepEqual(findCommitment(utf8(`a${text}b${text}`), c, ENCODING_BASE64URL), [1, 1 + text.length + 1]);
113
+ assert.deepEqual(findCommitment(utf8("nothing"), c, ENCODING_BASE64URL), []);
114
+ assert.deepEqual(findCommitment(utf8(text), c, "hex/1"), []);
115
+ });
116
+ });
117
+
118
+ describe("the canonical inline artifact (slot 2072, commit 2075)", () => {
119
+ test("CARRIED_INLINE, located, and named as carried by an encoding", async () => {
120
+ const r = await verifyFuse({ proof: liveProof, bytes: liveBytes });
121
+ assert.equal(r.category, "CARRIED_INLINE");
122
+ assert.equal(r.proof.valid, true);
123
+ assert.equal(r.reason, null);
124
+ assert.deepEqual(r.carriedBy, { kind: "encoding", id: ENCODING_BASE64URL });
125
+ assert.equal(r.placement, null, "an inline artifact has no placement: there is no recipe");
126
+ assert.equal(r.originDigestB64, null, "and no original");
127
+ assert.equal(r.offsets.length, 1);
128
+ // The bytes at the reported offset really are the commitment.
129
+ const at = Buffer.from(liveBytes.slice(r.offsets[0]!, r.offsets[0]! + 43)).toString("utf8");
130
+ assert.equal(at, r.slotCommitmentB64!.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""));
131
+ });
132
+
133
+ test("the span is the real one, and the statements name it", async () => {
134
+ const r = await verifyFuse({ proof: liveProof, bytes: liveBytes });
135
+ assert.equal(r.span?.slotCounter, "2072");
136
+ assert.equal(r.span?.commitCounter, "2075");
137
+ assert.match(r.statements[0] ?? "", /slot at position 2072/);
138
+ assert.match(r.statements[1] ?? "", /committed at position 2075/);
139
+ });
140
+
141
+ test("the limit rides with the claim and is carried by nothing else", async () => {
142
+ const r = await verifyFuse({ proof: liveProof, bytes: liveBytes });
143
+ assert.equal(r.limits.length, 1);
144
+ assert.match(r.limits[0] ?? "", /appended to a finished file pass the same check/);
145
+ const { proof, bytes } = await mint(() => utf8("no commitment in here"));
146
+ assert.deepEqual((await verifyFuse({ proof, bytes })).limits, [], "a result that establishes nothing carries no limits");
147
+ });
148
+
149
+ test("one edited byte is refused on the proof, not on the search", async () => {
150
+ const edited = new Uint8Array(liveBytes);
151
+ edited[edited.length - 2] = edited[edited.length - 2]! ^ 0x01;
152
+ const r = await verifyFuse({ proof: liveProof, bytes: edited });
153
+ assert.equal(r.category, "NO_MATCH", "the bytes are neither the artifact nor a declared original");
154
+ });
155
+
156
+ test("a measurement outside the pinned list is refused", async () => {
157
+ const r = await verifyFuse({ proof: liveProof, bytes: liveBytes, trustAnchors: { allowedMeasurements: ["0".repeat(96)] } });
158
+ assert.equal(r.category, "INVALID_UNDERLYING_PROOF");
159
+ });
160
+ });
161
+
162
+ describe("the other inline outcomes", () => {
163
+ test("every occurrence is counted and the first is the one the statement names", async () => {
164
+ const { proof, bytes } = await mint((c) => utf8(`${c}\nmiddle\n${c}\n`));
165
+ const r = await verifyFuse({ proof, bytes });
166
+ assert.equal(r.category, "CARRIED_INLINE");
167
+ assert.deepEqual(r.offsets.length, 2);
168
+ assert.equal(r.offsets[0], 0);
169
+ assert.match(r.statements[1] ?? "", /byte 0/);
170
+ });
171
+
172
+ test("COMMITMENT_ABSENT: the proof is valid, the bytes simply do not carry it", async () => {
173
+ const { proof, bytes } = await mint(() => utf8("nothing of the sort in here\n"));
174
+ const r = await verifyFuse({ proof, bytes });
175
+ assert.equal(r.category, "COMMITMENT_ABSENT");
176
+ assert.equal(r.proof.valid, true);
177
+ assert.deepEqual(r.offsets, []);
178
+ assert.deepEqual(r.statements, []);
179
+ });
180
+
181
+ test("an inline marker that declares an origin is refused: there is no original to name", async () => {
182
+ const { proof, bytes } = await mint((c) => utf8(`token ${c}\n`), { message: b64(new Uint8Array(32).fill(9)) });
183
+ const r = await verifyFuse({ proof, bytes });
184
+ assert.equal(r.category, "INVALID_ORIGIN_ATTRIBUTION");
185
+ assert.match(r.reason ?? "", /has no original/);
186
+ });
187
+
188
+ test("an unregistered title is UNDETERMINED_PLACEMENT, neither a pass nor a failure", async () => {
189
+ const { proof, bytes } = await mint((c) => utf8(`token ${c}\n`), { title: "hex/1" });
190
+ const r = await verifyFuse({ proof, bytes });
191
+ assert.equal(r.category, "UNDETERMINED_PLACEMENT");
192
+ });
193
+
194
+ test("a commitment appended to finished work passes, and the limit says so", async () => {
195
+ const { proof, bytes } = await mint((c) => utf8(`the work was already done\n${c}\n`));
196
+ const r = await verifyFuse({ proof, bytes });
197
+ assert.equal(r.category, "CARRIED_INLINE");
198
+ assert.ok((r.offsets[0] ?? 0) > 0);
199
+ assert.match(r.limits[0] ?? "", /producer's claim/);
200
+ });
201
+
202
+ test("a fused artifact still reports a placement, and carries no limits", async () => {
203
+ const fused = JSON.parse(readFileSync(FIX + "trailer.proof.json", "utf8")) as BitGraphProof;
204
+ const bytes = new Uint8Array(readFileSync(FIX + "fused-trailer.bin"));
205
+ const r = await verifyFuse({ proof: fused, bytes });
206
+ assert.equal(r.category, "FUSED_DIRECT");
207
+ assert.deepEqual(r.carriedBy, { kind: "placement", id: r.placement! });
208
+ assert.deepEqual(r.limits, []);
209
+ assert.deepEqual(r.offsets, []);
210
+ });
211
+ });
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
  import { describe, it } from "node:test";
3
3
  import * as assert from "node:assert/strict";
4
4
  import { placementForBytes, fusedNamesFor } from "../fuse.js";
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * BitGraph Fuse: reconstruction round trips (spec 15.3). A placement enters
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * fuse() against a fake transport built from real fixture proofs (minted
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * fuseSet() against a fake transport whose commit answer MINTS the proof from
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * BitGraph Sets, phase 1: the set manifest codec, the set/1 placement,
@@ -52,6 +52,28 @@ import type {
52
52
  import { makeKey, signBody, b64, utf8 } from "./audit-fixtures.js";
53
53
  import type { ManualKey } from "./audit-fixtures.js";
54
54
 
55
+ /* ⚠️ UPPERCASE THE WHOLE DIGEST, NOT ITS FIRST EIGHT CHARACTERS.
56
+ *
57
+ * Both call sites used to do `.replace(/"digest":"([0-9a-f]{8})/, upper)`, and
58
+ * that is a COIN FLIP. A manifest is sorted ascending by artifact digest, so
59
+ * the first digest in the text is the minimum of the set — which skews toward
60
+ * leading zeros and digits — and when all eight of those characters happen to
61
+ * be 0-9, uppercasing them changes nothing at all. The bytes stay canonical,
62
+ * parseSetManifest correctly accepts them, and the assertion that it should
63
+ * have REFUSED them fails. Measured: about 3-4% of runs, so roughly one red CI
64
+ * run in twenty-five, on a test that is not wrong about anything.
65
+ *
66
+ * A full 64-character hex digest with no letter in it has probability ~1e-15.
67
+ * And the assertion below turns the remaining impossibility into a loud
68
+ * failure rather than a silent no-op that passes for the wrong reason — which
69
+ * is what the old version did every time it did not flake.
70
+ */
71
+ function withUppercasedDigest(text: string): string {
72
+ const upper = text.replace(/"digest":"([0-9a-f]{64})"/, (_m, h: string) => `"digest":"${h.toUpperCase()}"`);
73
+ assert.notEqual(upper, text, "the fixture has no lowercase hex digest to uppercase; this check would pass vacuously");
74
+ return upper;
75
+ }
76
+
55
77
  const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
56
78
  const bytes = (name: string) => new Uint8Array(readFileSync(FIX + name));
57
79
  const proofOf = (name: string) => JSON.parse(readFileSync(FIX + name, "utf8")) as BitGraphProof;
@@ -296,7 +318,7 @@ describe("codec: parseSetManifest", () => {
296
318
 
297
319
  test("8. digests: uppercase hex, 63- and 65-char hex, and other algorithm spellings are refused", () => {
298
320
  const text = dec(A.manifest);
299
- assert.equal(parseSetManifest(enc(text.replace(/"digest":"([0-9a-f]{8})/, (_m, h: string) => `"digest":"${h.toUpperCase()}`))), null, "uppercase");
321
+ assert.equal(parseSetManifest(enc(withUppercasedDigest(text))), null, "uppercase");
300
322
  const o = asObject(A.manifest);
301
323
  const short = structuredClone(o); short.members[0]!.artifact.digest = short.members[0]!.artifact.digest.slice(0, 63);
302
324
  assert.equal(parseSetManifest(compact(short)), null, "63 chars");
@@ -770,7 +792,7 @@ describe("verifyFuseMember: manifest binding", () => {
770
792
  const text = dec(A.manifest);
771
793
  const bad: Array<[string, Uint8Array]> = [
772
794
  ["duplicate keys", enc(text.replace('"type":"bitgraph-fuse/1"', '"type":"bitgraph-fuse/1","type":"bitgraph-fuse/1"'))],
773
- ["uppercase hex", enc(text.replace(/"digest":"([0-9a-f]{8})/, (_m, h: string) => `"digest":"${h.toUpperCase()}`))],
795
+ ["uppercase hex", enc(withUppercasedDigest(text))],
774
796
  ["whitespace", enc(JSON.stringify(asObject(A.manifest), null, 1))],
775
797
  ];
776
798
  for (const [label, explicit] of bad) {
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * BitGraph Sets, set/2: a Merkle root over the rows instead of the list.
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * BitGraph Fuse: fixed vectors (spec 15.2).
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * BitGraph Fuse: verifier paths (spec 15.4, 15.5, 15.8) on real fixture
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  import { describe, test } from "node:test";
4
4
  import * as assert from "node:assert/strict";
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
  // End-to-end test for the BitGraph policy enforcement pipeline.
3
3
 
4
4
  import { test, describe } from "node:test";
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * Tests for verifyProofIntegrity, the bytes-free proof integrity API in
@@ -17,6 +17,7 @@ import {
17
17
  verify,
18
18
  verifyProofIntegrity,
19
19
  resetEpochLinkState,
20
+ createVerificationContext,
20
21
  canonicalize,
21
22
  } from "@mikeargento/bitgraph-verify";
22
23
  import type { BitGraphProof, ProofIntegrityResult } from "@mikeargento/bitgraph-verify";
@@ -388,6 +389,40 @@ describe("verifyProofIntegrity: epochLink", () => {
388
389
  assert.match(result.reason ?? "", /different keys/);
389
390
  });
390
391
 
392
+ /* A rejected proof must not change what the verifier accepts afterwards.
393
+ Before 2026-09-20 the single-successor map was written during the epoch
394
+ link check, which runs BEFORE the policy check, so a proof refused for an
395
+ untrusted measurement still claimed its predecessor and the next honest
396
+ proof was reported as a fork. Verification is a function of its inputs. */
397
+ test("a proof rejected by policy does not consume its predecessor", async () => {
398
+ const rejected = await makeEpochLinkFixture({ toEpochId: "epoch-successor-a" });
399
+ const honest = await makeEpochLinkFixture({ toEpochId: "epoch-successor-b" });
400
+
401
+ const r1 = await verifyProofIntegrity({
402
+ proof: rejected.proof,
403
+ trustAnchors: { allowedMeasurements: ["some-other-measurement"] },
404
+ });
405
+ assert.equal(r1.valid, false);
406
+
407
+ const r2 = await verifyProofIntegrity({ proof: honest.proof });
408
+ assert.equal(r2.valid, true, r2.valid ? "" : r2.reason);
409
+ });
410
+
411
+ test("an explicit context keeps one verification's history out of another's", async () => {
412
+ const a = await makeEpochLinkFixture({ toEpochId: "epoch-successor-a" });
413
+ const b = await makeEpochLinkFixture({ toEpochId: "epoch-successor-b" });
414
+ const one = createVerificationContext();
415
+ const two = createVerificationContext();
416
+
417
+ assert.equal((await verifyProofIntegrity({ proof: a.proof, context: one })).valid, true);
418
+ assert.equal((await verifyProofIntegrity({ proof: b.proof, context: two })).valid, true);
419
+
420
+ // The same context still catches the fork.
421
+ const forked = await verifyProofIntegrity({ proof: b.proof, context: one });
422
+ assert.equal(forked.valid, false);
423
+ assert.match(forked.reason ?? "", /FORK DETECTED/);
424
+ });
425
+
391
426
  test("detects a fork: same predecessor consumed by two successor epochs", async () => {
392
427
  const first = await makeEpochLinkFixture({ toEpochId: "epoch-successor-a" });
393
428
  const second = await makeEpochLinkFixture({ toEpochId: "epoch-successor-b" });
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  import { describe, test, before } from "node:test";
4
4
  import * as assert from "node:assert/strict";
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * bitgraph-core Constructor
package/src/fuse-cli.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
2
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
3
3
 
4
4
  /**
5
5
  * bitgraph-fuse: the internal harness (spec 9.2), as a command rather than a
package/src/fuse.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * fuse(builder, options): the producer interface of the bitgraph-fuse/1
package/src/host.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * bitgraph-core host abstraction
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * bitgraph-core — BitGraph
@@ -26,7 +26,8 @@ export type {
26
26
  WebAuthnAuthorization,
27
27
  AgencyEnvelope,
28
28
  } from "@mikeargento/bitgraph-verify";
29
- export { verify, resetEpochLinkState } from "@mikeargento/bitgraph-verify";
29
+ export { verify, resetEpochLinkState, createVerificationContext } from "@mikeargento/bitgraph-verify";
30
+ export type { VerificationContext } from "@mikeargento/bitgraph-verify";
30
31
  export type { VerifyResult } from "@mikeargento/bitgraph-verify";
31
32
  export { computeProofHash } from "@mikeargento/bitgraph-verify";
32
33
  export { canonicalize, canonicalizeToString, constantTimeEqual } from "@mikeargento/bitgraph-verify";
package/src/policy.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
1
+ // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
2
 
3
3
  /**
4
4
  * bitgraph-core Policy — parse, hash, and validate markdown policy documents.