@mikeargento/bitgraph-audit 0.1.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.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/anchors.d.ts +31 -0
- package/dist/anchors.d.ts.map +1 -0
- package/dist/anchors.js +116 -0
- package/dist/anchors.js.map +1 -0
- package/dist/anomalies.d.ts +42 -0
- package/dist/anomalies.d.ts.map +1 -0
- package/dist/anomalies.js +642 -0
- package/dist/anomalies.js.map +1 -0
- package/dist/attestation.d.ts +20 -0
- package/dist/attestation.d.ts.map +1 -0
- package/dist/attestation.js +795 -0
- package/dist/attestation.js.map +1 -0
- package/dist/audit.d.ts +19 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +116 -0
- package/dist/audit.js.map +1 -0
- package/dist/authority.d.ts +37 -0
- package/dist/authority.d.ts.map +1 -0
- package/dist/authority.js +126 -0
- package/dist/authority.js.map +1 -0
- package/dist/aws-nitro-root-ca.d.ts +24 -0
- package/dist/aws-nitro-root-ca.d.ts.map +1 -0
- package/dist/aws-nitro-root-ca.js +44 -0
- package/dist/aws-nitro-root-ca.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +257 -0
- package/dist/cli.js.map +1 -0
- package/dist/contents-hash.d.ts +30 -0
- package/dist/contents-hash.d.ts.map +1 -0
- package/dist/contents-hash.js +73 -0
- package/dist/contents-hash.js.map +1 -0
- package/dist/export.d.ts +64 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +192 -0
- package/dist/export.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest.d.ts +36 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +828 -0
- package/dist/ingest.js.map +1 -0
- package/dist/reconstruct.d.ts +40 -0
- package/dist/reconstruct.d.ts.map +1 -0
- package/dist/reconstruct.js +400 -0
- package/dist/reconstruct.js.map +1 -0
- package/dist/report-json.d.ts +3 -0
- package/dist/report-json.d.ts.map +1 -0
- package/dist/report-json.js +290 -0
- package/dist/report-json.js.map +1 -0
- package/dist/report-md.d.ts +3 -0
- package/dist/report-md.d.ts.map +1 -0
- package/dist/report-md.js +853 -0
- package/dist/report-md.js.map +1 -0
- package/dist/rlp.d.ts +37 -0
- package/dist/rlp.d.ts.map +1 -0
- package/dist/rlp.js +120 -0
- package/dist/rlp.js.map +1 -0
- package/dist/tar.d.ts +47 -0
- package/dist/tar.d.ts.map +1 -0
- package/dist/tar.js +483 -0
- package/dist/tar.js.map +1 -0
- package/dist/temporal.d.ts +55 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +461 -0
- package/dist/temporal.js.map +1 -0
- package/dist/types.d.ts +1389 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validity.d.ts +21 -0
- package/dist/validity.d.ts.map +1 -0
- package/dist/validity.js +86 -0
- package/dist/validity.js.map +1 -0
- package/dist/verify-tiers.d.ts +11 -0
- package/dist/verify-tiers.d.ts.map +1 -0
- package/dist/verify-tiers.js +120 -0
- package/dist/verify-tiers.js.map +1 -0
- package/dist/witness.d.ts +25 -0
- package/dist/witness.d.ts.map +1 -0
- package/dist/witness.js +308 -0
- package/dist/witness.js.map +1 -0
- package/package.json +42 -0
- package/src/anchors.ts +168 -0
- package/src/anomalies.ts +773 -0
- package/src/attestation.ts +878 -0
- package/src/audit.ts +136 -0
- package/src/authority.ts +165 -0
- package/src/aws-nitro-root-ca.ts +47 -0
- package/src/cli.ts +284 -0
- package/src/contents-hash.ts +95 -0
- package/src/export.ts +278 -0
- package/src/index.ts +122 -0
- package/src/ingest.ts +967 -0
- package/src/reconstruct.ts +491 -0
- package/src/report-json.ts +331 -0
- package/src/report-md.ts +1067 -0
- package/src/rlp.ts +142 -0
- package/src/tar.ts +587 -0
- package/src/temporal.ts +629 -0
- package/src/types.ts +1528 -0
- package/src/validity.ts +90 -0
- package/src/verify-tiers.ts +137 -0
- package/src/witness.ts +407 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic bundle contents hash, per docs/BUNDLE-FORMAT.md section 8.
|
|
5
|
+
*
|
|
6
|
+
* The hash is a file-level fixity value with no interpretation of any file:
|
|
7
|
+
*
|
|
8
|
+
* 1. Per entry: e = SHA-256(UTF-8(path) || 0x00 || contentBytes).
|
|
9
|
+
* Paths cannot contain NUL, so the separator is unambiguous.
|
|
10
|
+
* 2. Sort entries by raw UTF-8 path bytes (unsigned byte-wise comparison,
|
|
11
|
+
* no locale, no Unicode normalization).
|
|
12
|
+
* 3. contentsHashB64 = base64(SHA-256(concatenated 32-byte entry digests)).
|
|
13
|
+
*
|
|
14
|
+
* The hashed set is every bundle entry except the root manifest.json
|
|
15
|
+
* itself (which cannot cover its own hash). That exclusion is applied by
|
|
16
|
+
* callers; this module hashes exactly what it is given.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
20
|
+
|
|
21
|
+
const NUL = new Uint8Array([0]);
|
|
22
|
+
|
|
23
|
+
/** An in-memory bundle entry for one-shot hashing (producers, tests). */
|
|
24
|
+
export interface ContentsHashEntry {
|
|
25
|
+
/** Bundle-root-relative path: "/" separators, no leading "/" or "./". */
|
|
26
|
+
path: string;
|
|
27
|
+
/** Raw content bytes exactly as stored. */
|
|
28
|
+
content: Uint8Array;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A precomputed per-entry digest, for streaming consumers that hashed content incrementally. */
|
|
32
|
+
export interface ContentsHashEntryDigest {
|
|
33
|
+
path: string;
|
|
34
|
+
/** SHA-256(UTF-8(path) || 0x00 || content), 32 bytes. */
|
|
35
|
+
entryDigest: Uint8Array;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute the per-entry digest e = SHA-256(UTF-8(path) || 0x00 || content).
|
|
40
|
+
*/
|
|
41
|
+
export function computeEntryDigest(path: string, content: Uint8Array): Uint8Array {
|
|
42
|
+
const hasher = sha256.create();
|
|
43
|
+
hasher.update(encodePath(path));
|
|
44
|
+
hasher.update(NUL);
|
|
45
|
+
hasher.update(content);
|
|
46
|
+
return hasher.digest();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One-shot contents hash over an in-memory entry set.
|
|
51
|
+
*
|
|
52
|
+
* @throws {TypeError} if any path contains a NUL byte.
|
|
53
|
+
*/
|
|
54
|
+
export function computeContentsHashB64(entries: Iterable<ContentsHashEntry>): string {
|
|
55
|
+
const digests: ContentsHashEntryDigest[] = [];
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
digests.push({ path: entry.path, entryDigest: computeEntryDigest(entry.path, entry.content) });
|
|
58
|
+
}
|
|
59
|
+
return combineEntryDigests(digests);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Combine precomputed per-entry digests into the final contents hash:
|
|
64
|
+
* sort by raw UTF-8 path bytes, concatenate the 32-byte digests, SHA-256,
|
|
65
|
+
* base64-standard encode.
|
|
66
|
+
*/
|
|
67
|
+
export function combineEntryDigests(entries: Iterable<ContentsHashEntryDigest>): string {
|
|
68
|
+
const withPathBytes = Array.from(entries, (entry) => ({
|
|
69
|
+
pathBytes: encodePath(entry.path),
|
|
70
|
+
entryDigest: entry.entryDigest,
|
|
71
|
+
}));
|
|
72
|
+
withPathBytes.sort((a, b) => compareBytes(a.pathBytes, b.pathBytes));
|
|
73
|
+
|
|
74
|
+
const outer = sha256.create();
|
|
75
|
+
for (const entry of withPathBytes) {
|
|
76
|
+
outer.update(entry.entryDigest);
|
|
77
|
+
}
|
|
78
|
+
return Buffer.from(outer.digest()).toString("base64");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function encodePath(path: string): Uint8Array {
|
|
82
|
+
if (path.includes("\0")) {
|
|
83
|
+
throw new TypeError("bitgraph-audit: bundle entry paths must not contain NUL bytes");
|
|
84
|
+
}
|
|
85
|
+
return new TextEncoder().encode(path);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
|
89
|
+
const n = Math.min(a.length, b.length);
|
|
90
|
+
for (let i = 0; i < n; i++) {
|
|
91
|
+
const d = (a[i] as number) - (b[i] as number);
|
|
92
|
+
if (d !== 0) return d;
|
|
93
|
+
}
|
|
94
|
+
return a.length - b.length;
|
|
95
|
+
}
|
package/src/export.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bundle archive builder: the producer counterpart of ingest.
|
|
5
|
+
*
|
|
6
|
+
* buildBundleArchive() assembles a spec-conformant bitgraph-bundle/1
|
|
7
|
+
* .tar.gz archive (docs/BUNDLE-FORMAT.md) from in-memory proofs, anchor
|
|
8
|
+
* proofs, anchor witnesses, and optional artifact bytes. This is generic,
|
|
9
|
+
* read-side-safe bundle assembly: it never talks to a ledger, never reads
|
|
10
|
+
* the clock (the advisory generatedAt string is passed in by the caller),
|
|
11
|
+
* and never touches the network.
|
|
12
|
+
*
|
|
13
|
+
* Deterministic by construction: entries are written in raw-UTF-8-path-byte
|
|
14
|
+
* order, tar header metadata is fixed (mtime 0, mode 0644, uid/gid 0), and
|
|
15
|
+
* the manifest is serialized with a fixed field order. The same input
|
|
16
|
+
* produces a byte-identical archive on the same runtime (the gzip layer's
|
|
17
|
+
* bytes are fixed by zlib for a given input, so cross-runtime identity
|
|
18
|
+
* additionally assumes the same zlib).
|
|
19
|
+
*
|
|
20
|
+
* Producer conformance (spec section 12.1) is enforced, not advised:
|
|
21
|
+
* non-bitgraph/1 proofs, duplicate canonical identities, unsafe paths, and
|
|
22
|
+
* a storedProofHash that disagrees with the computed canonical hash all
|
|
23
|
+
* throw instead of producing a bundle that would audit dirty.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { gzipSync } from "node:zlib";
|
|
27
|
+
import { computeProofHash } from "@mikeargento/bitgraph-verify";
|
|
28
|
+
import { computeContentsHashB64, type ContentsHashEntry } from "./contents-hash.js";
|
|
29
|
+
import { writeTarArchive } from "./tar.js";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Input types
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/** One member proof to include. */
|
|
36
|
+
export interface BundleArchiveProofEntry {
|
|
37
|
+
/** The proof object (wire form or stored form). Must be version "bitgraph/1". */
|
|
38
|
+
proof: Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* The stored proofHash to append as a trailing field (ledger stored form).
|
|
41
|
+
* Must equal the computed canonical proof hash; a mismatch throws, because
|
|
42
|
+
* shipping it would produce a proofhash-mismatch finding in every audit.
|
|
43
|
+
* Omit to serialize the proof object as given.
|
|
44
|
+
*/
|
|
45
|
+
storedProofHash?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One anchor witness file to include (spec section 10.2). */
|
|
49
|
+
export interface BundleArchiveWitnessEntry {
|
|
50
|
+
/** Bundle-root-relative path, e.g. "witnesses/123456.json" per the advisory layout. */
|
|
51
|
+
path: string;
|
|
52
|
+
/** The witness object, serialized verbatim as compact JSON. */
|
|
53
|
+
witness: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Original artifact bytes to include. */
|
|
57
|
+
export interface BundleArchiveArtifactFile {
|
|
58
|
+
/** File name (may contain subdirectories); placed under "artifacts/". Advisory, never load-bearing. */
|
|
59
|
+
name: string;
|
|
60
|
+
bytes: Uint8Array;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface BundleArchiveInput {
|
|
64
|
+
/** Member proofs, placed under proofs/ per the advisory layout. */
|
|
65
|
+
proofs: BundleArchiveProofEntry[];
|
|
66
|
+
/**
|
|
67
|
+
* Anchor proofs, placed under anchors/ per the advisory layout. They are
|
|
68
|
+
* ordinary bitgraph/1 members; the separate group only controls placement.
|
|
69
|
+
* A proof must appear in exactly one group (duplicates throw).
|
|
70
|
+
*/
|
|
71
|
+
anchors?: BundleArchiveProofEntry[];
|
|
72
|
+
witnesses?: BundleArchiveWitnessEntry[];
|
|
73
|
+
artifactFiles?: BundleArchiveArtifactFile[];
|
|
74
|
+
/** Advisory manifest field: distinct commit.epochId values, raw base64 form. */
|
|
75
|
+
epochIds: string[];
|
|
76
|
+
/** Advisory manifest field: distinct chain ids ("global" for the default chain). */
|
|
77
|
+
chainIds: string[];
|
|
78
|
+
/** Advisory manifest field: open-epoch snapshot declaration. Emitted only when non-empty. */
|
|
79
|
+
openEpochs?: Array<{ epochId: string; counterAtSnapshot: string }>;
|
|
80
|
+
/**
|
|
81
|
+
* Advisory manifest field: ISO 8601 UTC generation time, supplied BY THE
|
|
82
|
+
* CALLER. This function never reads the clock, so the same input always
|
|
83
|
+
* produces the same archive.
|
|
84
|
+
*/
|
|
85
|
+
generatedAt?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Builder
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Assemble a spec-conformant bitgraph-bundle/1 .tar.gz archive.
|
|
94
|
+
*
|
|
95
|
+
* @returns the gzipped tar bytes.
|
|
96
|
+
* @throws {TypeError} on producer-conformance violations: a proof whose
|
|
97
|
+
* version is not exactly "bitgraph/1", a storedProofHash that does not
|
|
98
|
+
* match the computed canonical hash, duplicate canonical proof identities,
|
|
99
|
+
* unsafe or colliding entry paths, or a witness placed at the reserved
|
|
100
|
+
* root manifest.json path.
|
|
101
|
+
*/
|
|
102
|
+
export function buildBundleArchive(input: BundleArchiveInput): Uint8Array {
|
|
103
|
+
const entries: ContentsHashEntry[] = [];
|
|
104
|
+
const usedPaths = new Set<string>();
|
|
105
|
+
const seenProofHashes = new Set<string>();
|
|
106
|
+
const partitions = new Map<string, { epochId: string; chainId: string; min: bigint; max: bigint }>();
|
|
107
|
+
|
|
108
|
+
const addEntry = (path: string, content: Uint8Array): void => {
|
|
109
|
+
assertSafePath(path);
|
|
110
|
+
if (path === "manifest.json") {
|
|
111
|
+
throw new TypeError(
|
|
112
|
+
'bitgraph-audit: "manifest.json" is the reserved root manifest path; entries may not claim it'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (usedPaths.has(path)) {
|
|
116
|
+
throw new TypeError(`bitgraph-audit: duplicate bundle entry path "${path}"`);
|
|
117
|
+
}
|
|
118
|
+
usedPaths.add(path);
|
|
119
|
+
entries.push({ path, content });
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const addProofGroup = (group: BundleArchiveProofEntry[], prefix: "proofs" | "anchors"): void => {
|
|
123
|
+
for (const entry of group) {
|
|
124
|
+
const proof = entry.proof;
|
|
125
|
+
if (proof["version"] !== "bitgraph/1") {
|
|
126
|
+
throw new TypeError(
|
|
127
|
+
`bitgraph-audit: only bitgraph/1 proofs are bundle members; got version ${JSON.stringify(
|
|
128
|
+
proof["version"]
|
|
129
|
+
)}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const canonicalHash = computeProofHash(proof);
|
|
133
|
+
if (entry.storedProofHash !== undefined && entry.storedProofHash !== canonicalHash) {
|
|
134
|
+
throw new TypeError(
|
|
135
|
+
"bitgraph-audit: storedProofHash does not match the computed canonical proof hash; " +
|
|
136
|
+
"refusing to produce a bundle that would carry a proofhash-mismatch finding"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (seenProofHashes.has(canonicalHash)) {
|
|
140
|
+
throw new TypeError(
|
|
141
|
+
`bitgraph-audit: duplicate proof (canonical hash ${canonicalHash}); ` +
|
|
142
|
+
"each member must appear exactly once or the bundle audits with duplicate findings"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
seenProofHashes.add(canonicalHash);
|
|
146
|
+
|
|
147
|
+
const commit = isPlainObject(proof["commit"]) ? (proof["commit"] as Record<string, unknown>) : {};
|
|
148
|
+
const epochId = typeof commit["epochId"] === "string" && commit["epochId"].length > 0
|
|
149
|
+
? (commit["epochId"] as string)
|
|
150
|
+
: undefined;
|
|
151
|
+
const counter = typeof commit["counter"] === "string" && commit["counter"].length > 0
|
|
152
|
+
? (commit["counter"] as string)
|
|
153
|
+
: undefined;
|
|
154
|
+
const chainId = typeof commit["chainId"] === "string" && commit["chainId"].length > 0
|
|
155
|
+
? (commit["chainId"] as string)
|
|
156
|
+
: "global";
|
|
157
|
+
|
|
158
|
+
const path =
|
|
159
|
+
epochId !== undefined && counter !== undefined
|
|
160
|
+
? `${prefix}/${toSafeId(epochId)}/${counter.padStart(12, "0")}-${toSafeId(canonicalHash)}.json`
|
|
161
|
+
: `${prefix}/unpositioned/${toSafeId(canonicalHash)}.json`;
|
|
162
|
+
|
|
163
|
+
const body =
|
|
164
|
+
entry.storedProofHash !== undefined
|
|
165
|
+
? { ...proof, proofHash: entry.storedProofHash }
|
|
166
|
+
: proof;
|
|
167
|
+
addEntry(path, utf8(JSON.stringify(body)));
|
|
168
|
+
|
|
169
|
+
if (epochId !== undefined && counter !== undefined) {
|
|
170
|
+
let value: bigint | undefined;
|
|
171
|
+
try {
|
|
172
|
+
value = BigInt(counter);
|
|
173
|
+
} catch {
|
|
174
|
+
value = undefined;
|
|
175
|
+
}
|
|
176
|
+
if (value !== undefined && value >= 0n) {
|
|
177
|
+
const key = `${epochId}\u0000${chainId}`;
|
|
178
|
+
const existing = partitions.get(key);
|
|
179
|
+
if (existing === undefined) {
|
|
180
|
+
partitions.set(key, { epochId, chainId, min: value, max: value });
|
|
181
|
+
} else {
|
|
182
|
+
if (value < existing.min) existing.min = value;
|
|
183
|
+
if (value > existing.max) existing.max = value;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
addProofGroup(input.proofs, "proofs");
|
|
191
|
+
addProofGroup(input.anchors ?? [], "anchors");
|
|
192
|
+
|
|
193
|
+
for (const witness of input.witnesses ?? []) {
|
|
194
|
+
addEntry(witness.path, utf8(JSON.stringify(witness.witness)));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const artifact of input.artifactFiles ?? []) {
|
|
198
|
+
if (artifact.name.length === 0) {
|
|
199
|
+
throw new TypeError("bitgraph-audit: artifact file names must be non-empty");
|
|
200
|
+
}
|
|
201
|
+
addEntry(`artifacts/${artifact.name}`, artifact.bytes);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Contents hash over every entry except the manifest itself (spec section 8).
|
|
205
|
+
const contentsHashB64 = computeContentsHashB64(entries);
|
|
206
|
+
|
|
207
|
+
// Manifest with a fixed field order (spec section 7.1). Compact JSON.
|
|
208
|
+
const counterRanges = Array.from(partitions.values())
|
|
209
|
+
.sort((a, b) =>
|
|
210
|
+
a.epochId < b.epochId ? -1 : a.epochId > b.epochId ? 1 : a.chainId < b.chainId ? -1 : a.chainId > b.chainId ? 1 : 0
|
|
211
|
+
)
|
|
212
|
+
.map((p) => ({ epochId: p.epochId, chainId: p.chainId, min: p.min.toString(), max: p.max.toString() }));
|
|
213
|
+
|
|
214
|
+
const manifest: Record<string, unknown> = { version: "bitgraph-bundle/1" };
|
|
215
|
+
manifest["epochIds"] = input.epochIds;
|
|
216
|
+
manifest["chainIds"] = input.chainIds;
|
|
217
|
+
manifest["proofCount"] = seenProofHashes.size;
|
|
218
|
+
manifest["counterRanges"] = counterRanges;
|
|
219
|
+
if (input.generatedAt !== undefined) manifest["generatedAt"] = input.generatedAt;
|
|
220
|
+
manifest["contentsHashB64"] = contentsHashB64;
|
|
221
|
+
manifest["artifactsIncluded"] = (input.artifactFiles?.length ?? 0) > 0;
|
|
222
|
+
if (input.openEpochs !== undefined && input.openEpochs.length > 0) {
|
|
223
|
+
manifest["openEpochs"] = input.openEpochs;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const manifestPath = "manifest.json";
|
|
227
|
+
assertSafePath(manifestPath);
|
|
228
|
+
entries.push({ path: manifestPath, content: utf8(JSON.stringify(manifest)) });
|
|
229
|
+
|
|
230
|
+
// Fixed entry ordering: raw UTF-8 path bytes, unsigned byte-wise.
|
|
231
|
+
const sorted = entries
|
|
232
|
+
.map((entry) => ({ entry, pathBytes: utf8(entry.path) }))
|
|
233
|
+
.sort((a, b) => compareBytes(a.pathBytes, b.pathBytes))
|
|
234
|
+
.map(({ entry }) => ({ path: entry.path, content: entry.content }));
|
|
235
|
+
|
|
236
|
+
const tarBytes = writeTarArchive(sorted);
|
|
237
|
+
const gz = gzipSync(tarBytes, { level: 9 });
|
|
238
|
+
return new Uint8Array(gz.buffer, gz.byteOffset, gz.byteLength);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Helpers
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
/** Path-safe transform for base64 values in file names (spec section 9). */
|
|
246
|
+
function toSafeId(b64: string): string {
|
|
247
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Spec section 4.1: relative, no NUL, no "..", no leading "/" or "./", no empty segments. */
|
|
251
|
+
function assertSafePath(path: string): void {
|
|
252
|
+
if (path.length === 0) throw new TypeError("bitgraph-audit: bundle entry paths must be non-empty");
|
|
253
|
+
if (path.includes("\0")) throw new TypeError("bitgraph-audit: bundle entry paths must not contain NUL bytes");
|
|
254
|
+
if (path.startsWith("/")) throw new TypeError(`bitgraph-audit: bundle entry path "${path}" must be relative`);
|
|
255
|
+
const segments = path.split("/");
|
|
256
|
+
for (const segment of segments) {
|
|
257
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
258
|
+
throw new TypeError(`bitgraph-audit: bundle entry path "${path}" contains an unsafe component`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
264
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function utf8(text: string): Uint8Array {
|
|
268
|
+
return new TextEncoder().encode(text);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
|
272
|
+
const n = Math.min(a.length, b.length);
|
|
273
|
+
for (let i = 0; i < n; i++) {
|
|
274
|
+
const d = (a[i] as number) - (b[i] as number);
|
|
275
|
+
if (d !== 0) return d;
|
|
276
|
+
}
|
|
277
|
+
return a.length - b.length;
|
|
278
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @mikeargento/bitgraph-audit
|
|
5
|
+
*
|
|
6
|
+
* Offline audit of BitGraph proof bundles. The operator's database is not
|
|
7
|
+
* the evidence: proofs are portable objects, and this package lets an
|
|
8
|
+
* independent reader ingest a bundle (directory, .tar, or .tar.gz), verify
|
|
9
|
+
* the available evidence through the canonical verify package, and build
|
|
10
|
+
* the indexes that causal reconstruction and reporting run on.
|
|
11
|
+
*
|
|
12
|
+
* Zero runtime network access, by design. Everything verifiable is
|
|
13
|
+
* verified locally; everything else is reported as exactly what it is.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type {
|
|
17
|
+
AnomalyCode,
|
|
18
|
+
AuditFinding,
|
|
19
|
+
VerificationTier,
|
|
20
|
+
VerificationStatus,
|
|
21
|
+
EmbeddedProofHashStatus,
|
|
22
|
+
ProofVerification,
|
|
23
|
+
ProofSource,
|
|
24
|
+
ObservedProof,
|
|
25
|
+
UnsupportedVersionRecord,
|
|
26
|
+
ArtifactRecord,
|
|
27
|
+
AnchorWitnessFile,
|
|
28
|
+
BundleManifest,
|
|
29
|
+
ManifestReport,
|
|
30
|
+
ContainerKind,
|
|
31
|
+
IngestCounts,
|
|
32
|
+
IngestLimits,
|
|
33
|
+
IngestResult,
|
|
34
|
+
VerifyObservedOptions,
|
|
35
|
+
VerificationSummary,
|
|
36
|
+
MatchedArtifactBytes,
|
|
37
|
+
PartitionKey,
|
|
38
|
+
ChainComponent,
|
|
39
|
+
ChainPartition,
|
|
40
|
+
EpochLinkFields,
|
|
41
|
+
EpochLineageEdge,
|
|
42
|
+
EpochAnchorBound,
|
|
43
|
+
EpochRecord,
|
|
44
|
+
EpochRelationshipResult,
|
|
45
|
+
ReconstructionResult,
|
|
46
|
+
ChainAnomaly,
|
|
47
|
+
UnexplainedPositionsDetail,
|
|
48
|
+
DivergenceKind,
|
|
49
|
+
DivergenceParty,
|
|
50
|
+
DivergenceRecord,
|
|
51
|
+
AnomalyReport,
|
|
52
|
+
AttestedMeasurementEvidence,
|
|
53
|
+
AuthorityGroup,
|
|
54
|
+
SignerEpochSpan,
|
|
55
|
+
AuthorityAnalysis,
|
|
56
|
+
AnchorMetadataCorroboration,
|
|
57
|
+
AnchorRecord,
|
|
58
|
+
AnchorIdentification,
|
|
59
|
+
AnchorWitnessOutcome,
|
|
60
|
+
AnchorWitnessAnalysis,
|
|
61
|
+
BoundEvidence,
|
|
62
|
+
SegmentBound,
|
|
63
|
+
TemporalSegmentStatus,
|
|
64
|
+
TemporalSegment,
|
|
65
|
+
AnchorOrderedPair,
|
|
66
|
+
TemporalAnalysis,
|
|
67
|
+
AttestationCheck,
|
|
68
|
+
NitroValidationOptions,
|
|
69
|
+
NitroValidationResult,
|
|
70
|
+
ProofAttestationRecord,
|
|
71
|
+
AttestationAnalysis,
|
|
72
|
+
AuditOptions,
|
|
73
|
+
AuditRunMetadata,
|
|
74
|
+
AuditResult,
|
|
75
|
+
ExitFlags,
|
|
76
|
+
AnomalyStage,
|
|
77
|
+
ReportAnomaly,
|
|
78
|
+
ReportProofRecord,
|
|
79
|
+
ReportPartition,
|
|
80
|
+
UnorderedEpochPair,
|
|
81
|
+
ReportEpochRelationships,
|
|
82
|
+
ReportInputSummary,
|
|
83
|
+
ReportSummary,
|
|
84
|
+
AuditJsonReport,
|
|
85
|
+
} from "./types.js";
|
|
86
|
+
|
|
87
|
+
export { ingestBundle, streamMatchedArtifacts, DEFAULT_INGEST_LIMITS } from "./ingest.js";
|
|
88
|
+
|
|
89
|
+
export { verifyObservedProofs } from "./verify-tiers.js";
|
|
90
|
+
|
|
91
|
+
export { reconstructChains } from "./reconstruct.js";
|
|
92
|
+
|
|
93
|
+
export { classifyAnomalies } from "./anomalies.js";
|
|
94
|
+
|
|
95
|
+
export { analyzeAuthorities } from "./authority.js";
|
|
96
|
+
|
|
97
|
+
export { identifyAnchors } from "./anchors.js";
|
|
98
|
+
|
|
99
|
+
export { verifyAnchorWitnesses, verifyAnchorWitness } from "./witness.js";
|
|
100
|
+
|
|
101
|
+
export { deriveTemporalBounds } from "./temporal.js";
|
|
102
|
+
|
|
103
|
+
export { validateAttestations, validateNitroAttestationDocument } from "./attestation.js";
|
|
104
|
+
|
|
105
|
+
export { runAudit, computeExitFlags, auditToolVersion } from "./audit.js";
|
|
106
|
+
|
|
107
|
+
export { buildJsonReport } from "./report-json.js";
|
|
108
|
+
|
|
109
|
+
export { buildMarkdownReport } from "./report-md.js";
|
|
110
|
+
|
|
111
|
+
export { AWS_NITRO_ROOT_CA_PEM } from "./aws-nitro-root-ca.js";
|
|
112
|
+
|
|
113
|
+
export { computeContentsHashB64, computeEntryDigest } from "./contents-hash.js";
|
|
114
|
+
export type { ContentsHashEntry } from "./contents-hash.js";
|
|
115
|
+
|
|
116
|
+
export { buildBundleArchive } from "./export.js";
|
|
117
|
+
export type {
|
|
118
|
+
BundleArchiveInput,
|
|
119
|
+
BundleArchiveProofEntry,
|
|
120
|
+
BundleArchiveWitnessEntry,
|
|
121
|
+
BundleArchiveArtifactFile,
|
|
122
|
+
} from "./export.js";
|