@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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
|
|
3
|
+
/**
|
|
4
|
+
* bitgraph-audit CLI: run the full offline audit pipeline over a bundle
|
|
5
|
+
* and write audit-report.json and/or audit-report.md.
|
|
6
|
+
*
|
|
7
|
+
* Report content goes to files only; stdout carries a short completion
|
|
8
|
+
* line naming the written files and the exit meaning. The CLI performs
|
|
9
|
+
* no network access. Arguments are parsed with plain process.argv
|
|
10
|
+
* handling, no dependency.
|
|
11
|
+
*
|
|
12
|
+
* Exit codes are bit flags (documented in --help and on the ExitFlags
|
|
13
|
+
* type): 0 clean, 1 verification failures (including unsupported-version
|
|
14
|
+
* rejections), 2 chain or authority anomalies, divergences between valid
|
|
15
|
+
* proofs, or anchor witness verification failures, 3 both, 64 usage or
|
|
16
|
+
* input error (no report was produced).
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { auditToolVersion, computeExitFlags, runAudit } from "./audit.js";
|
|
22
|
+
import { buildJsonReport } from "./report-json.js";
|
|
23
|
+
import { buildMarkdownReport } from "./report-md.js";
|
|
24
|
+
const USAGE_EXIT_CODE = 64;
|
|
25
|
+
/** The 14 fields of the canonical VerificationPolicy (verify package, G7). */
|
|
26
|
+
const KNOWN_POLICY_FIELDS = [
|
|
27
|
+
"requireEnforcement",
|
|
28
|
+
"allowedMeasurements",
|
|
29
|
+
"allowedPublicKeys",
|
|
30
|
+
"requireAttestation",
|
|
31
|
+
"requireAttestationFormat",
|
|
32
|
+
"minCounter",
|
|
33
|
+
"maxCounter",
|
|
34
|
+
"minTime",
|
|
35
|
+
"maxTime",
|
|
36
|
+
"requireEpochId",
|
|
37
|
+
"requireActor",
|
|
38
|
+
"allowedActorKeyIds",
|
|
39
|
+
"allowedActorProviders",
|
|
40
|
+
"requireSlot",
|
|
41
|
+
];
|
|
42
|
+
class UsageError extends Error {
|
|
43
|
+
}
|
|
44
|
+
const USAGE_LINE = "Usage: bitgraph-audit <path-to-bundle> [--out <dir>] [--format json,md] [--trust-policy <path>]";
|
|
45
|
+
function helpText() {
|
|
46
|
+
return [
|
|
47
|
+
`bitgraph-audit ${auditToolVersion()}: offline audit of BitGraph proof bundles.`,
|
|
48
|
+
"",
|
|
49
|
+
USAGE_LINE,
|
|
50
|
+
"",
|
|
51
|
+
"The bundle may be a directory, a .tar archive, or a .tar.gz/.tgz",
|
|
52
|
+
"archive. The audit runs entirely offline: no RPC, no HTTP, no DNS.",
|
|
53
|
+
"",
|
|
54
|
+
"Options:",
|
|
55
|
+
" --out <dir> Directory to write the report files into",
|
|
56
|
+
" (default: current directory; created if missing).",
|
|
57
|
+
" --format <list> Comma-separated formats to write: json, md",
|
|
58
|
+
" (default: json,md). json writes audit-report.json;",
|
|
59
|
+
" md writes audit-report.md.",
|
|
60
|
+
" --trust-policy <path> JSON file parsed as the canonical",
|
|
61
|
+
" VerificationPolicy and applied at both",
|
|
62
|
+
" verification tiers. Valid fields:",
|
|
63
|
+
` ${KNOWN_POLICY_FIELDS.join(", ")}.`,
|
|
64
|
+
" Any other field is an error.",
|
|
65
|
+
" --help, -h Print this help and exit 0.",
|
|
66
|
+
"",
|
|
67
|
+
"Exit codes (bit flags):",
|
|
68
|
+
" 0 Clean: no verification failures, no chain anomalies, no",
|
|
69
|
+
" divergences.",
|
|
70
|
+
" 1 Verification failures: at least one proof failed its canonical",
|
|
71
|
+
" cryptographic checks at either tier, or at least one",
|
|
72
|
+
" proof-shaped input was rejected as an unsupported version",
|
|
73
|
+
" (only bitgraph/1 is supported). A proof whose artifact bytes",
|
|
74
|
+
" are absent from the bundle is NOT a failure by itself: its",
|
|
75
|
+
" bytes-free checks decide, unless a supplied trust policy makes",
|
|
76
|
+
" them fail (for example requireSlot), in which case it counts",
|
|
77
|
+
" here.",
|
|
78
|
+
" 2 Chain or authority anomalies, divergences between valid proofs,",
|
|
79
|
+
" or anchor witness verification failures: unexplained counter",
|
|
80
|
+
" positions, chain breaks, collisions, cross-kind position reuse,",
|
|
81
|
+
" forks, authority changes inside an epoch, epoch link anomalies,",
|
|
82
|
+
" or a supplied anchor witness that fails its offline verification",
|
|
83
|
+
" (block-hash mismatch, digest binding, block number, header or",
|
|
84
|
+
" RLP malformation, an invalid candidate anchor, or an unmatched",
|
|
85
|
+
" witness). Benign findings are reported but never set exit bits:",
|
|
86
|
+
" duplicate copies, manifest advisories, unsafe paths, embedded",
|
|
87
|
+
" proofHash mismatches, and informational anchor findings (unsigned",
|
|
88
|
+
" metadata disagreements, metadata-only anchor claims, and",
|
|
89
|
+
" unparseable anchor titles, all overridden by the signed body).",
|
|
90
|
+
" 3 Both 1 and 2.",
|
|
91
|
+
" 64 Usage or input error: unknown option, unreadable bundle, or",
|
|
92
|
+
" invalid trust policy. No report was produced.",
|
|
93
|
+
"",
|
|
94
|
+
"Attestation validation results are always reported in full but never",
|
|
95
|
+
"change the exit code on their own: an invalid attestation document on",
|
|
96
|
+
"an otherwise verified proof is reported without affecting the exit",
|
|
97
|
+
"code, and counts under exit bit 1 only when a supplied trust policy",
|
|
98
|
+
"made verification itself fail.",
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
function parseArgs(argv) {
|
|
102
|
+
let bundlePath;
|
|
103
|
+
let outDir = ".";
|
|
104
|
+
let formats = new Set(["json", "md"]);
|
|
105
|
+
let trustPolicyPath;
|
|
106
|
+
const takeValue = (flag, index) => {
|
|
107
|
+
const value = argv[index + 1];
|
|
108
|
+
if (value === undefined)
|
|
109
|
+
throw new UsageError(`${flag} requires a value.`);
|
|
110
|
+
return value;
|
|
111
|
+
};
|
|
112
|
+
for (let i = 0; i < argv.length; i++) {
|
|
113
|
+
const arg = argv[i];
|
|
114
|
+
if (arg === "--help" || arg === "-h")
|
|
115
|
+
return "help";
|
|
116
|
+
if (arg === "--out") {
|
|
117
|
+
outDir = takeValue(arg, i);
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
else if (arg === "--format") {
|
|
121
|
+
const raw = takeValue(arg, i);
|
|
122
|
+
i++;
|
|
123
|
+
const parts = raw
|
|
124
|
+
.split(",")
|
|
125
|
+
.map((p) => p.trim())
|
|
126
|
+
.filter((p) => p.length > 0);
|
|
127
|
+
if (parts.length === 0)
|
|
128
|
+
throw new UsageError("--format requires at least one of: json, md.");
|
|
129
|
+
formats = new Set();
|
|
130
|
+
for (const part of parts) {
|
|
131
|
+
if (part !== "json" && part !== "md") {
|
|
132
|
+
throw new UsageError(`unknown format "${part}". Valid formats: json, md.`);
|
|
133
|
+
}
|
|
134
|
+
formats.add(part);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (arg === "--trust-policy") {
|
|
138
|
+
trustPolicyPath = takeValue(arg, i);
|
|
139
|
+
i++;
|
|
140
|
+
}
|
|
141
|
+
else if (arg.startsWith("-")) {
|
|
142
|
+
throw new UsageError(`unknown option "${arg}".`);
|
|
143
|
+
}
|
|
144
|
+
else if (bundlePath === undefined) {
|
|
145
|
+
bundlePath = arg;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
throw new UsageError("more than one bundle path was given.");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (bundlePath === undefined)
|
|
152
|
+
throw new UsageError("missing bundle path.");
|
|
153
|
+
return {
|
|
154
|
+
bundlePath,
|
|
155
|
+
outDir,
|
|
156
|
+
formats,
|
|
157
|
+
...(trustPolicyPath !== undefined ? { trustPolicyPath } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function loadTrustPolicy(path) {
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = readFileSync(path, "utf8");
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
throw new UsageError(`cannot read trust policy file "${path}": ${err instanceof Error ? err.message : String(err)}`);
|
|
167
|
+
}
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(raw);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
throw new UsageError(`trust policy file "${path}" is not valid JSON.`);
|
|
174
|
+
}
|
|
175
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
176
|
+
throw new UsageError("trust policy must be a JSON object.");
|
|
177
|
+
}
|
|
178
|
+
const unknown = Object.keys(parsed).filter((key) => !KNOWN_POLICY_FIELDS.includes(key));
|
|
179
|
+
if (unknown.length > 0) {
|
|
180
|
+
throw new UsageError(`unknown trust policy ${unknown.length === 1 ? "field" : "fields"}: ` +
|
|
181
|
+
`${unknown.join(", ")}. Valid fields: ${KNOWN_POLICY_FIELDS.join(", ")}.`);
|
|
182
|
+
}
|
|
183
|
+
return parsed;
|
|
184
|
+
}
|
|
185
|
+
function exitMeaning(flags) {
|
|
186
|
+
if (flags.code === 0)
|
|
187
|
+
return "clean: no verification failures, no chain anomalies, no divergences";
|
|
188
|
+
const parts = [];
|
|
189
|
+
if (flags.verificationFailures)
|
|
190
|
+
parts.push("verification failures");
|
|
191
|
+
if (flags.chainAnomaliesOrDivergences) {
|
|
192
|
+
parts.push("chain anomalies, divergences, or anchor witness verification failures");
|
|
193
|
+
}
|
|
194
|
+
return parts.join("; ");
|
|
195
|
+
}
|
|
196
|
+
async function main() {
|
|
197
|
+
let parsed;
|
|
198
|
+
try {
|
|
199
|
+
parsed = parseArgs(process.argv.slice(2));
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (err instanceof UsageError) {
|
|
203
|
+
process.stderr.write(`bitgraph-audit: ${err.message}\n${USAGE_LINE}\nSee --help for details.\n`);
|
|
204
|
+
return USAGE_EXIT_CODE;
|
|
205
|
+
}
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
if (parsed === "help") {
|
|
209
|
+
process.stdout.write(helpText() + "\n");
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
let trustAnchors;
|
|
213
|
+
try {
|
|
214
|
+
if (parsed.trustPolicyPath !== undefined) {
|
|
215
|
+
trustAnchors = loadTrustPolicy(parsed.trustPolicyPath);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (err instanceof UsageError) {
|
|
220
|
+
process.stderr.write(`bitgraph-audit: ${err.message}\n${USAGE_LINE}\nSee --help for details.\n`);
|
|
221
|
+
return USAGE_EXIT_CODE;
|
|
222
|
+
}
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
let result;
|
|
226
|
+
try {
|
|
227
|
+
result = await runAudit(parsed.bundlePath, trustAnchors !== undefined ? { trustAnchors } : undefined);
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
process.stderr.write(`bitgraph-audit: cannot audit "${parsed.bundlePath}": ` +
|
|
231
|
+
`${err instanceof Error ? err.message : String(err)}\n`);
|
|
232
|
+
return USAGE_EXIT_CODE;
|
|
233
|
+
}
|
|
234
|
+
await mkdir(parsed.outDir, { recursive: true });
|
|
235
|
+
const written = [];
|
|
236
|
+
if (parsed.formats.has("json")) {
|
|
237
|
+
const path = join(parsed.outDir, "audit-report.json");
|
|
238
|
+
await writeFile(path, JSON.stringify(buildJsonReport(result), null, 2) + "\n", "utf8");
|
|
239
|
+
written.push(path);
|
|
240
|
+
}
|
|
241
|
+
if (parsed.formats.has("md")) {
|
|
242
|
+
const path = join(parsed.outDir, "audit-report.md");
|
|
243
|
+
await writeFile(path, buildMarkdownReport(result), "utf8");
|
|
244
|
+
written.push(path);
|
|
245
|
+
}
|
|
246
|
+
const flags = computeExitFlags(result);
|
|
247
|
+
process.stdout.write(`bitgraph-audit ${auditToolVersion()}: wrote ${written.join(", ")}\n` +
|
|
248
|
+
`exit ${flags.code}: ${exitMeaning(flags)}\n`);
|
|
249
|
+
return flags.code;
|
|
250
|
+
}
|
|
251
|
+
main().then((code) => {
|
|
252
|
+
process.exitCode = code;
|
|
253
|
+
}, (err) => {
|
|
254
|
+
process.stderr.write(`bitgraph-audit: unexpected error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
|
|
255
|
+
process.exitCode = USAGE_EXIT_CODE;
|
|
256
|
+
});
|
|
257
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,qFAAqF;AAErF;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAGrD,MAAM,eAAe,GAAG,EAAE,CAAC;AAE3B,8EAA8E;AAC9E,MAAM,mBAAmB,GAAsB;IAC7C,oBAAoB;IACpB,qBAAqB;IACrB,mBAAmB;IACnB,oBAAoB;IACpB,0BAA0B;IAC1B,YAAY;IACZ,YAAY;IACZ,SAAS;IACT,SAAS;IACT,gBAAgB;IAChB,cAAc;IACd,oBAAoB;IACpB,uBAAuB;IACvB,aAAa;CACd,CAAC;AAEF,MAAM,UAAW,SAAQ,KAAK;CAAG;AAEjC,MAAM,UAAU,GACd,iGAAiG,CAAC;AAEpG,SAAS,QAAQ;IACf,OAAO;QACL,kBAAkB,gBAAgB,EAAE,4CAA4C;QAChF,EAAE;QACF,UAAU;QACV,EAAE;QACF,kEAAkE;QAClE,oEAAoE;QACpE,EAAE;QACF,UAAU;QACV,mEAAmE;QACnE,4EAA4E;QAC5E,qEAAqE;QACrE,6EAA6E;QAC7E,qDAAqD;QACrD,4DAA4D;QAC5D,iEAAiE;QACjE,4DAA4D;QAC5D,4BAA4B,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;QAC7D,uDAAuD;QACvD,sDAAsD;QACtD,EAAE;QACF,yBAAyB;QACzB,+DAA+D;QAC/D,oBAAoB;QACpB,sEAAsE;QACtE,4DAA4D;QAC5D,iEAAiE;QACjE,oEAAoE;QACpE,kEAAkE;QAClE,sEAAsE;QACtE,oEAAoE;QACpE,aAAa;QACb,uEAAuE;QACvE,oEAAoE;QACpE,uEAAuE;QACvE,uEAAuE;QACvE,wEAAwE;QACxE,qEAAqE;QACrE,sEAAsE;QACtE,uEAAuE;QACvE,qEAAqE;QACrE,yEAAyE;QACzE,gEAAgE;QAChE,sEAAsE;QACtE,qBAAqB;QACrB,mEAAmE;QACnE,qDAAqD;QACrD,EAAE;QACF,sEAAsE;QACtE,uEAAuE;QACvE,oEAAoE;QACpE,qEAAqE;QACrE,gCAAgC;KACjC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AASD,SAAS,SAAS,CAAC,IAAc;IAC/B,IAAI,UAA8B,CAAC;IACnC,IAAI,MAAM,GAAG,GAAG,CAAC;IACjB,IAAI,OAAO,GAAuB,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IAC1D,IAAI,eAAmC,CAAC;IAExC,MAAM,SAAS,GAAG,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE;QACxD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,oBAAoB,CAAC,CAAC;QAC3E,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAW,CAAC;QAC9B,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,MAAM,CAAC;QACpD,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;YACpB,MAAM,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YAC3B,CAAC,EAAE,CAAC;QACN,CAAC;aAAM,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YAC9B,CAAC,EAAE,CAAC;YACJ,MAAM,KAAK,GAAG,GAAG;iBACd,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,MAAM,IAAI,UAAU,CAAC,8CAA8C,CAAC,CAAC;YAC7F,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC;YACpB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;oBACrC,MAAM,IAAI,UAAU,CAAC,mBAAmB,IAAI,6BAA6B,CAAC,CAAC;gBAC7E,CAAC;gBACD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;aAAM,IAAI,GAAG,KAAK,gBAAgB,EAAE,CAAC;YACpC,eAAe,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACpC,CAAC,EAAE,CAAC;QACN,CAAC;aAAM,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,UAAU,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;QACnD,CAAC;aAAM,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YACpC,UAAU,GAAG,GAAG,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,UAAU,CAAC,sCAAsC,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IAED,IAAI,UAAU,KAAK,SAAS;QAAE,MAAM,IAAI,UAAU,CAAC,sBAAsB,CAAC,CAAC;IAC3E,OAAO;QACL,UAAU;QACV,MAAM;QACN,OAAO;QACP,GAAG,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9D,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,UAAU,CAClB,kCAAkC,IAAI,MAAM,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC/F,CAAC;IACJ,CAAC;IACD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,UAAU,CAAC,sBAAsB,IAAI,sBAAsB,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,UAAU,CAAC,qCAAqC,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,mBAAmB,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IACxF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,UAAU,CAClB,wBAAwB,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,IAAI;YACnE,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5E,CAAC;IACJ,CAAC;IACD,OAAO,MAA4B,CAAC;AACtC,CAAC;AAED,SAAS,WAAW,CAAC,KAAgB;IACnC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,qEAAqE,CAAC;IACnG,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK,CAAC,oBAAoB;QAAE,KAAK,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IACpE,IAAI,KAAK,CAAC,2BAA2B,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;IACtF,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,MAA2B,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;YAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,CAAC,OAAO,KAAK,UAAU,6BAA6B,CAAC,CAAC;YACjG,OAAO,eAAe,CAAC;QACzB,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,CAAC;QACxC,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,YAA4C,CAAC;IACjD,IAAI,CAAC;QACH,IAAI,MAAM,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;YACzC,YAAY,GAAG,eAAe,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;YAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,CAAC,OAAO,KAAK,UAAU,6BAA6B,CAAC,CAAC;YACjG,OAAO,eAAe,CAAC;QACzB,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,QAAQ,CACrB,MAAM,CAAC,UAAU,EACjB,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,CAC1D,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iCAAiC,MAAM,CAAC,UAAU,KAAK;YACrD,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAC1D,CAAC;QACF,OAAO,eAAe,CAAC;IACzB,CAAC;IAED,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;QACtD,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;QACvF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;QACpD,MAAM,SAAS,CAAC,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAC3D,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAED,MAAM,KAAK,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,kBAAkB,gBAAgB,EAAE,WAAW,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;QACnE,QAAQ,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,KAAK,CAAC,IAAI,CAChD,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED,IAAI,EAAE,CAAC,IAAI,CACT,CAAC,IAAI,EAAE,EAAE;IACP,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC,EACD,CAAC,GAAG,EAAE,EAAE;IACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qCAAqC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CACzG,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,eAAe,CAAC;AACrC,CAAC,CACF,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** An in-memory bundle entry for one-shot hashing (producers, tests). */
|
|
2
|
+
export interface ContentsHashEntry {
|
|
3
|
+
/** Bundle-root-relative path: "/" separators, no leading "/" or "./". */
|
|
4
|
+
path: string;
|
|
5
|
+
/** Raw content bytes exactly as stored. */
|
|
6
|
+
content: Uint8Array;
|
|
7
|
+
}
|
|
8
|
+
/** A precomputed per-entry digest, for streaming consumers that hashed content incrementally. */
|
|
9
|
+
export interface ContentsHashEntryDigest {
|
|
10
|
+
path: string;
|
|
11
|
+
/** SHA-256(UTF-8(path) || 0x00 || content), 32 bytes. */
|
|
12
|
+
entryDigest: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Compute the per-entry digest e = SHA-256(UTF-8(path) || 0x00 || content).
|
|
16
|
+
*/
|
|
17
|
+
export declare function computeEntryDigest(path: string, content: Uint8Array): Uint8Array;
|
|
18
|
+
/**
|
|
19
|
+
* One-shot contents hash over an in-memory entry set.
|
|
20
|
+
*
|
|
21
|
+
* @throws {TypeError} if any path contains a NUL byte.
|
|
22
|
+
*/
|
|
23
|
+
export declare function computeContentsHashB64(entries: Iterable<ContentsHashEntry>): string;
|
|
24
|
+
/**
|
|
25
|
+
* Combine precomputed per-entry digests into the final contents hash:
|
|
26
|
+
* sort by raw UTF-8 path bytes, concatenate the 32-byte digests, SHA-256,
|
|
27
|
+
* base64-standard encode.
|
|
28
|
+
*/
|
|
29
|
+
export declare function combineEntryDigests(entries: Iterable<ContentsHashEntryDigest>): string;
|
|
30
|
+
//# sourceMappingURL=contents-hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contents-hash.d.ts","sourceRoot":"","sources":["../src/contents-hash.ts"],"names":[],"mappings":"AAsBA,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,iGAAiG;AACjG,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,WAAW,EAAE,UAAU,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,UAAU,CAMhF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAMnF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,QAAQ,CAAC,uBAAuB,CAAC,GAAG,MAAM,CAYtF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic bundle contents hash, per docs/BUNDLE-FORMAT.md section 8.
|
|
4
|
+
*
|
|
5
|
+
* The hash is a file-level fixity value with no interpretation of any file:
|
|
6
|
+
*
|
|
7
|
+
* 1. Per entry: e = SHA-256(UTF-8(path) || 0x00 || contentBytes).
|
|
8
|
+
* Paths cannot contain NUL, so the separator is unambiguous.
|
|
9
|
+
* 2. Sort entries by raw UTF-8 path bytes (unsigned byte-wise comparison,
|
|
10
|
+
* no locale, no Unicode normalization).
|
|
11
|
+
* 3. contentsHashB64 = base64(SHA-256(concatenated 32-byte entry digests)).
|
|
12
|
+
*
|
|
13
|
+
* The hashed set is every bundle entry except the root manifest.json
|
|
14
|
+
* itself (which cannot cover its own hash). That exclusion is applied by
|
|
15
|
+
* callers; this module hashes exactly what it is given.
|
|
16
|
+
*/
|
|
17
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
18
|
+
const NUL = new Uint8Array([0]);
|
|
19
|
+
/**
|
|
20
|
+
* Compute the per-entry digest e = SHA-256(UTF-8(path) || 0x00 || content).
|
|
21
|
+
*/
|
|
22
|
+
export function computeEntryDigest(path, content) {
|
|
23
|
+
const hasher = sha256.create();
|
|
24
|
+
hasher.update(encodePath(path));
|
|
25
|
+
hasher.update(NUL);
|
|
26
|
+
hasher.update(content);
|
|
27
|
+
return hasher.digest();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One-shot contents hash over an in-memory entry set.
|
|
31
|
+
*
|
|
32
|
+
* @throws {TypeError} if any path contains a NUL byte.
|
|
33
|
+
*/
|
|
34
|
+
export function computeContentsHashB64(entries) {
|
|
35
|
+
const digests = [];
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
digests.push({ path: entry.path, entryDigest: computeEntryDigest(entry.path, entry.content) });
|
|
38
|
+
}
|
|
39
|
+
return combineEntryDigests(digests);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Combine precomputed per-entry digests into the final contents hash:
|
|
43
|
+
* sort by raw UTF-8 path bytes, concatenate the 32-byte digests, SHA-256,
|
|
44
|
+
* base64-standard encode.
|
|
45
|
+
*/
|
|
46
|
+
export function combineEntryDigests(entries) {
|
|
47
|
+
const withPathBytes = Array.from(entries, (entry) => ({
|
|
48
|
+
pathBytes: encodePath(entry.path),
|
|
49
|
+
entryDigest: entry.entryDigest,
|
|
50
|
+
}));
|
|
51
|
+
withPathBytes.sort((a, b) => compareBytes(a.pathBytes, b.pathBytes));
|
|
52
|
+
const outer = sha256.create();
|
|
53
|
+
for (const entry of withPathBytes) {
|
|
54
|
+
outer.update(entry.entryDigest);
|
|
55
|
+
}
|
|
56
|
+
return Buffer.from(outer.digest()).toString("base64");
|
|
57
|
+
}
|
|
58
|
+
function encodePath(path) {
|
|
59
|
+
if (path.includes("\0")) {
|
|
60
|
+
throw new TypeError("bitgraph-audit: bundle entry paths must not contain NUL bytes");
|
|
61
|
+
}
|
|
62
|
+
return new TextEncoder().encode(path);
|
|
63
|
+
}
|
|
64
|
+
function compareBytes(a, b) {
|
|
65
|
+
const n = Math.min(a.length, b.length);
|
|
66
|
+
for (let i = 0; i < n; i++) {
|
|
67
|
+
const d = a[i] - b[i];
|
|
68
|
+
if (d !== 0)
|
|
69
|
+
return d;
|
|
70
|
+
}
|
|
71
|
+
return a.length - b.length;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=contents-hash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contents-hash.js","sourceRoot":"","sources":["../src/contents-hash.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAErF;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAE9C,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAiBhC;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,OAAmB;IAClE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;IAC/B,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IAChC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACnB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvB,OAAO,MAAM,CAAC,MAAM,EAAE,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAoC;IACzE,MAAM,OAAO,GAA8B,EAAE,CAAC;IAC9C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,kBAAkB,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACjG,CAAC;IACD,OAAO,mBAAmB,CAAC,OAAO,CAAC,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA0C;IAC5E,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACpD,SAAS,EAAE,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;QACjC,WAAW,EAAE,KAAK,CAAC,WAAW;KAC/B,CAAC,CAAC,CAAC;IACJ,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAErE,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,SAAS,CAAC,+DAA+D,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC;AAED,SAAS,YAAY,CAAC,CAAa,EAAE,CAAa;IAChD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAI,CAAC,CAAC,CAAC,CAAY,GAAI,CAAC,CAAC,CAAC,CAAY,CAAC;QAC9C,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;AAC7B,CAAC"}
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** One member proof to include. */
|
|
2
|
+
export interface BundleArchiveProofEntry {
|
|
3
|
+
/** The proof object (wire form or stored form). Must be version "bitgraph/1". */
|
|
4
|
+
proof: Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* The stored proofHash to append as a trailing field (ledger stored form).
|
|
7
|
+
* Must equal the computed canonical proof hash; a mismatch throws, because
|
|
8
|
+
* shipping it would produce a proofhash-mismatch finding in every audit.
|
|
9
|
+
* Omit to serialize the proof object as given.
|
|
10
|
+
*/
|
|
11
|
+
storedProofHash?: string;
|
|
12
|
+
}
|
|
13
|
+
/** One anchor witness file to include (spec section 10.2). */
|
|
14
|
+
export interface BundleArchiveWitnessEntry {
|
|
15
|
+
/** Bundle-root-relative path, e.g. "witnesses/123456.json" per the advisory layout. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** The witness object, serialized verbatim as compact JSON. */
|
|
18
|
+
witness: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/** Original artifact bytes to include. */
|
|
21
|
+
export interface BundleArchiveArtifactFile {
|
|
22
|
+
/** File name (may contain subdirectories); placed under "artifacts/". Advisory, never load-bearing. */
|
|
23
|
+
name: string;
|
|
24
|
+
bytes: Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
export interface BundleArchiveInput {
|
|
27
|
+
/** Member proofs, placed under proofs/ per the advisory layout. */
|
|
28
|
+
proofs: BundleArchiveProofEntry[];
|
|
29
|
+
/**
|
|
30
|
+
* Anchor proofs, placed under anchors/ per the advisory layout. They are
|
|
31
|
+
* ordinary bitgraph/1 members; the separate group only controls placement.
|
|
32
|
+
* A proof must appear in exactly one group (duplicates throw).
|
|
33
|
+
*/
|
|
34
|
+
anchors?: BundleArchiveProofEntry[];
|
|
35
|
+
witnesses?: BundleArchiveWitnessEntry[];
|
|
36
|
+
artifactFiles?: BundleArchiveArtifactFile[];
|
|
37
|
+
/** Advisory manifest field: distinct commit.epochId values, raw base64 form. */
|
|
38
|
+
epochIds: string[];
|
|
39
|
+
/** Advisory manifest field: distinct chain ids ("global" for the default chain). */
|
|
40
|
+
chainIds: string[];
|
|
41
|
+
/** Advisory manifest field: open-epoch snapshot declaration. Emitted only when non-empty. */
|
|
42
|
+
openEpochs?: Array<{
|
|
43
|
+
epochId: string;
|
|
44
|
+
counterAtSnapshot: string;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Advisory manifest field: ISO 8601 UTC generation time, supplied BY THE
|
|
48
|
+
* CALLER. This function never reads the clock, so the same input always
|
|
49
|
+
* produces the same archive.
|
|
50
|
+
*/
|
|
51
|
+
generatedAt?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Assemble a spec-conformant bitgraph-bundle/1 .tar.gz archive.
|
|
55
|
+
*
|
|
56
|
+
* @returns the gzipped tar bytes.
|
|
57
|
+
* @throws {TypeError} on producer-conformance violations: a proof whose
|
|
58
|
+
* version is not exactly "bitgraph/1", a storedProofHash that does not
|
|
59
|
+
* match the computed canonical hash, duplicate canonical proof identities,
|
|
60
|
+
* unsafe or colliding entry paths, or a witness placed at the reserved
|
|
61
|
+
* root manifest.json path.
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildBundleArchive(input: BundleArchiveInput): Uint8Array;
|
|
64
|
+
//# sourceMappingURL=export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"AAkCA,mCAAmC;AACnC,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,8DAA8D;AAC9D,MAAM,WAAW,yBAAyB;IACxC,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB;IACxC,uGAAuG;IACvG,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,mEAAmE;IACnE,MAAM,EAAE,uBAAuB,EAAE,CAAC;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,uBAAuB,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,yBAAyB,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,yBAAyB,EAAE,CAAC;IAC5C,gFAAgF;IAChF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oFAAoF;IACpF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,6FAA6F;IAC7F,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnE;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,UAAU,CAyIxE"}
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
|
|
2
|
+
/**
|
|
3
|
+
* Bundle archive builder: the producer counterpart of ingest.
|
|
4
|
+
*
|
|
5
|
+
* buildBundleArchive() assembles a spec-conformant bitgraph-bundle/1
|
|
6
|
+
* .tar.gz archive (docs/BUNDLE-FORMAT.md) from in-memory proofs, anchor
|
|
7
|
+
* proofs, anchor witnesses, and optional artifact bytes. This is generic,
|
|
8
|
+
* read-side-safe bundle assembly: it never talks to a ledger, never reads
|
|
9
|
+
* the clock (the advisory generatedAt string is passed in by the caller),
|
|
10
|
+
* and never touches the network.
|
|
11
|
+
*
|
|
12
|
+
* Deterministic by construction: entries are written in raw-UTF-8-path-byte
|
|
13
|
+
* order, tar header metadata is fixed (mtime 0, mode 0644, uid/gid 0), and
|
|
14
|
+
* the manifest is serialized with a fixed field order. The same input
|
|
15
|
+
* produces a byte-identical archive on the same runtime (the gzip layer's
|
|
16
|
+
* bytes are fixed by zlib for a given input, so cross-runtime identity
|
|
17
|
+
* additionally assumes the same zlib).
|
|
18
|
+
*
|
|
19
|
+
* Producer conformance (spec section 12.1) is enforced, not advised:
|
|
20
|
+
* non-bitgraph/1 proofs, duplicate canonical identities, unsafe paths, and
|
|
21
|
+
* a storedProofHash that disagrees with the computed canonical hash all
|
|
22
|
+
* throw instead of producing a bundle that would audit dirty.
|
|
23
|
+
*/
|
|
24
|
+
import { gzipSync } from "node:zlib";
|
|
25
|
+
import { computeProofHash } from "@mikeargento/bitgraph-verify";
|
|
26
|
+
import { computeContentsHashB64 } from "./contents-hash.js";
|
|
27
|
+
import { writeTarArchive } from "./tar.js";
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Builder
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
/**
|
|
32
|
+
* Assemble a spec-conformant bitgraph-bundle/1 .tar.gz archive.
|
|
33
|
+
*
|
|
34
|
+
* @returns the gzipped tar bytes.
|
|
35
|
+
* @throws {TypeError} on producer-conformance violations: a proof whose
|
|
36
|
+
* version is not exactly "bitgraph/1", a storedProofHash that does not
|
|
37
|
+
* match the computed canonical hash, duplicate canonical proof identities,
|
|
38
|
+
* unsafe or colliding entry paths, or a witness placed at the reserved
|
|
39
|
+
* root manifest.json path.
|
|
40
|
+
*/
|
|
41
|
+
export function buildBundleArchive(input) {
|
|
42
|
+
const entries = [];
|
|
43
|
+
const usedPaths = new Set();
|
|
44
|
+
const seenProofHashes = new Set();
|
|
45
|
+
const partitions = new Map();
|
|
46
|
+
const addEntry = (path, content) => {
|
|
47
|
+
assertSafePath(path);
|
|
48
|
+
if (path === "manifest.json") {
|
|
49
|
+
throw new TypeError('bitgraph-audit: "manifest.json" is the reserved root manifest path; entries may not claim it');
|
|
50
|
+
}
|
|
51
|
+
if (usedPaths.has(path)) {
|
|
52
|
+
throw new TypeError(`bitgraph-audit: duplicate bundle entry path "${path}"`);
|
|
53
|
+
}
|
|
54
|
+
usedPaths.add(path);
|
|
55
|
+
entries.push({ path, content });
|
|
56
|
+
};
|
|
57
|
+
const addProofGroup = (group, prefix) => {
|
|
58
|
+
for (const entry of group) {
|
|
59
|
+
const proof = entry.proof;
|
|
60
|
+
if (proof["version"] !== "bitgraph/1") {
|
|
61
|
+
throw new TypeError(`bitgraph-audit: only bitgraph/1 proofs are bundle members; got version ${JSON.stringify(proof["version"])}`);
|
|
62
|
+
}
|
|
63
|
+
const canonicalHash = computeProofHash(proof);
|
|
64
|
+
if (entry.storedProofHash !== undefined && entry.storedProofHash !== canonicalHash) {
|
|
65
|
+
throw new TypeError("bitgraph-audit: storedProofHash does not match the computed canonical proof hash; " +
|
|
66
|
+
"refusing to produce a bundle that would carry a proofhash-mismatch finding");
|
|
67
|
+
}
|
|
68
|
+
if (seenProofHashes.has(canonicalHash)) {
|
|
69
|
+
throw new TypeError(`bitgraph-audit: duplicate proof (canonical hash ${canonicalHash}); ` +
|
|
70
|
+
"each member must appear exactly once or the bundle audits with duplicate findings");
|
|
71
|
+
}
|
|
72
|
+
seenProofHashes.add(canonicalHash);
|
|
73
|
+
const commit = isPlainObject(proof["commit"]) ? proof["commit"] : {};
|
|
74
|
+
const epochId = typeof commit["epochId"] === "string" && commit["epochId"].length > 0
|
|
75
|
+
? commit["epochId"]
|
|
76
|
+
: undefined;
|
|
77
|
+
const counter = typeof commit["counter"] === "string" && commit["counter"].length > 0
|
|
78
|
+
? commit["counter"]
|
|
79
|
+
: undefined;
|
|
80
|
+
const chainId = typeof commit["chainId"] === "string" && commit["chainId"].length > 0
|
|
81
|
+
? commit["chainId"]
|
|
82
|
+
: "global";
|
|
83
|
+
const path = epochId !== undefined && counter !== undefined
|
|
84
|
+
? `${prefix}/${toSafeId(epochId)}/${counter.padStart(12, "0")}-${toSafeId(canonicalHash)}.json`
|
|
85
|
+
: `${prefix}/unpositioned/${toSafeId(canonicalHash)}.json`;
|
|
86
|
+
const body = entry.storedProofHash !== undefined
|
|
87
|
+
? { ...proof, proofHash: entry.storedProofHash }
|
|
88
|
+
: proof;
|
|
89
|
+
addEntry(path, utf8(JSON.stringify(body)));
|
|
90
|
+
if (epochId !== undefined && counter !== undefined) {
|
|
91
|
+
let value;
|
|
92
|
+
try {
|
|
93
|
+
value = BigInt(counter);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
value = undefined;
|
|
97
|
+
}
|
|
98
|
+
if (value !== undefined && value >= 0n) {
|
|
99
|
+
const key = `${epochId}\u0000${chainId}`;
|
|
100
|
+
const existing = partitions.get(key);
|
|
101
|
+
if (existing === undefined) {
|
|
102
|
+
partitions.set(key, { epochId, chainId, min: value, max: value });
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (value < existing.min)
|
|
106
|
+
existing.min = value;
|
|
107
|
+
if (value > existing.max)
|
|
108
|
+
existing.max = value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
addProofGroup(input.proofs, "proofs");
|
|
115
|
+
addProofGroup(input.anchors ?? [], "anchors");
|
|
116
|
+
for (const witness of input.witnesses ?? []) {
|
|
117
|
+
addEntry(witness.path, utf8(JSON.stringify(witness.witness)));
|
|
118
|
+
}
|
|
119
|
+
for (const artifact of input.artifactFiles ?? []) {
|
|
120
|
+
if (artifact.name.length === 0) {
|
|
121
|
+
throw new TypeError("bitgraph-audit: artifact file names must be non-empty");
|
|
122
|
+
}
|
|
123
|
+
addEntry(`artifacts/${artifact.name}`, artifact.bytes);
|
|
124
|
+
}
|
|
125
|
+
// Contents hash over every entry except the manifest itself (spec section 8).
|
|
126
|
+
const contentsHashB64 = computeContentsHashB64(entries);
|
|
127
|
+
// Manifest with a fixed field order (spec section 7.1). Compact JSON.
|
|
128
|
+
const counterRanges = Array.from(partitions.values())
|
|
129
|
+
.sort((a, b) => a.epochId < b.epochId ? -1 : a.epochId > b.epochId ? 1 : a.chainId < b.chainId ? -1 : a.chainId > b.chainId ? 1 : 0)
|
|
130
|
+
.map((p) => ({ epochId: p.epochId, chainId: p.chainId, min: p.min.toString(), max: p.max.toString() }));
|
|
131
|
+
const manifest = { version: "bitgraph-bundle/1" };
|
|
132
|
+
manifest["epochIds"] = input.epochIds;
|
|
133
|
+
manifest["chainIds"] = input.chainIds;
|
|
134
|
+
manifest["proofCount"] = seenProofHashes.size;
|
|
135
|
+
manifest["counterRanges"] = counterRanges;
|
|
136
|
+
if (input.generatedAt !== undefined)
|
|
137
|
+
manifest["generatedAt"] = input.generatedAt;
|
|
138
|
+
manifest["contentsHashB64"] = contentsHashB64;
|
|
139
|
+
manifest["artifactsIncluded"] = (input.artifactFiles?.length ?? 0) > 0;
|
|
140
|
+
if (input.openEpochs !== undefined && input.openEpochs.length > 0) {
|
|
141
|
+
manifest["openEpochs"] = input.openEpochs;
|
|
142
|
+
}
|
|
143
|
+
const manifestPath = "manifest.json";
|
|
144
|
+
assertSafePath(manifestPath);
|
|
145
|
+
entries.push({ path: manifestPath, content: utf8(JSON.stringify(manifest)) });
|
|
146
|
+
// Fixed entry ordering: raw UTF-8 path bytes, unsigned byte-wise.
|
|
147
|
+
const sorted = entries
|
|
148
|
+
.map((entry) => ({ entry, pathBytes: utf8(entry.path) }))
|
|
149
|
+
.sort((a, b) => compareBytes(a.pathBytes, b.pathBytes))
|
|
150
|
+
.map(({ entry }) => ({ path: entry.path, content: entry.content }));
|
|
151
|
+
const tarBytes = writeTarArchive(sorted);
|
|
152
|
+
const gz = gzipSync(tarBytes, { level: 9 });
|
|
153
|
+
return new Uint8Array(gz.buffer, gz.byteOffset, gz.byteLength);
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Helpers
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
/** Path-safe transform for base64 values in file names (spec section 9). */
|
|
159
|
+
function toSafeId(b64) {
|
|
160
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
161
|
+
}
|
|
162
|
+
/** Spec section 4.1: relative, no NUL, no "..", no leading "/" or "./", no empty segments. */
|
|
163
|
+
function assertSafePath(path) {
|
|
164
|
+
if (path.length === 0)
|
|
165
|
+
throw new TypeError("bitgraph-audit: bundle entry paths must be non-empty");
|
|
166
|
+
if (path.includes("\0"))
|
|
167
|
+
throw new TypeError("bitgraph-audit: bundle entry paths must not contain NUL bytes");
|
|
168
|
+
if (path.startsWith("/"))
|
|
169
|
+
throw new TypeError(`bitgraph-audit: bundle entry path "${path}" must be relative`);
|
|
170
|
+
const segments = path.split("/");
|
|
171
|
+
for (const segment of segments) {
|
|
172
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
173
|
+
throw new TypeError(`bitgraph-audit: bundle entry path "${path}" contains an unsafe component`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function isPlainObject(value) {
|
|
178
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
179
|
+
}
|
|
180
|
+
function utf8(text) {
|
|
181
|
+
return new TextEncoder().encode(text);
|
|
182
|
+
}
|
|
183
|
+
function compareBytes(a, b) {
|
|
184
|
+
const n = Math.min(a.length, b.length);
|
|
185
|
+
for (let i = 0; i < n; i++) {
|
|
186
|
+
const d = a[i] - b[i];
|
|
187
|
+
if (d !== 0)
|
|
188
|
+
return d;
|
|
189
|
+
}
|
|
190
|
+
return a.length - b.length;
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=export.js.map
|