verifyhash 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 +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- package/trustledger/valueproof.js +350 -0
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/journal-cli.js — the DISK-BACKED, verb-shaped surface over the pure INTEGRITY-JOURNAL CORE (T-60.2).
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS IS A SEPARATE FILE FROM cli/journal.js
|
|
6
|
+
// cli/journal.js is the PURE core (T-60.1): no I/O, no key, no network. That purity is a hard acceptance
|
|
7
|
+
// criterion a STATIC grep in test/journal.core.test.js enforces on the WHOLE core file — it must not even
|
|
8
|
+
// `require("fs")`. This module is where the filesystem/CLI wiring lives so the core stays provably pure:
|
|
9
|
+
// it reads the fs and runs the EXISTING composed verify path, but still holds NO signing material and
|
|
10
|
+
// opens NO socket (append/verify are pure-local file ops).
|
|
11
|
+
//
|
|
12
|
+
// THE ON-DISK JOURNAL FORMAT — newline-delimited JSON (JSONL)
|
|
13
|
+
// One entry per line, appended in order. JSONL is chosen precisely because an APPEND is STRICTLY ADDITIVE:
|
|
14
|
+
// `fs.appendFileSync` writes exactly the new line's bytes and never rewrites a prior line, so the
|
|
15
|
+
// pre-existing bytes are preserved verbatim (a test asserts prefix-preservation byte-for-byte). A hand-edit
|
|
16
|
+
// to any past line changes that entry's stored fields, so `computeEntryHash` no longer re-derives its
|
|
17
|
+
// `entryHash` and `verifyJournal` LOCALIZES the break at that line's `seq`.
|
|
18
|
+
//
|
|
19
|
+
// THE EXIT-CODE CONTRACT (shared with `vh verify` / `vh evidence verify`)
|
|
20
|
+
// 0 = journal OK / verify ACCEPTED, 3 = drift / broken chain, 2 = usage error, 1 = IO error. This is the
|
|
21
|
+
// SAME 0/3 CI contract the composed verify path uses; a test asserts parity against it (evidence.EXIT).
|
|
22
|
+
|
|
23
|
+
const fs = require("fs");
|
|
24
|
+
const path = require("path");
|
|
25
|
+
const evidence = require("./evidence");
|
|
26
|
+
const { verifyRequest, VERDICT } = require("./serve-verify");
|
|
27
|
+
const {
|
|
28
|
+
appendEntry,
|
|
29
|
+
verifyJournal,
|
|
30
|
+
canonicalize,
|
|
31
|
+
JournalError,
|
|
32
|
+
} = require("./journal");
|
|
33
|
+
const journalLog = require("./journal-log");
|
|
34
|
+
|
|
35
|
+
// The shared verify exit-code contract (0 ok / 3 drift / 2 usage / 1 IO). Re-declared from the SAME values
|
|
36
|
+
// evidence.EXIT / the CLI verbs use so a test can assert parity rather than trusting a comment.
|
|
37
|
+
const JOURNAL_EXIT = Object.freeze({ OK: 0, IO: 1, USAGE: 2, DRIFT: 3 });
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------------------------------
|
|
40
|
+
// T-63.2 — the transparency-log surface over the T-63.1 ordered Merkle-log core (cli/journal-log.js).
|
|
41
|
+
// Four STRICTLY-ADDITIVE, VERIFY-ONLY subcommands: tree-head / prove-inclusion / prove-consistency /
|
|
42
|
+
// check-proof. All four are read-only (the ONLY write is the --out proof artifact the caller names),
|
|
43
|
+
// hold NO key, and bind NO network. `check-proof` is the OFFLINE third-party AUDITOR path: it reads
|
|
44
|
+
// ONLY the proof artifact — NEVER the journal — so an auditor can confirm inclusion/append-only-ness
|
|
45
|
+
// without ever holding the log (a test runs it with NO journal present under an fs+network guard).
|
|
46
|
+
// ---------------------------------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
// The self-describing proof-artifact kinds (documented schema; T-64.2's witness path consumes these).
|
|
49
|
+
const JOURNAL_INCLUSION_PROOF_KIND = "vh-journal-inclusion";
|
|
50
|
+
const JOURNAL_CONSISTENCY_PROOF_KIND = "vh-journal-consistency";
|
|
51
|
+
|
|
52
|
+
// The SAME honesty boundary the journal already carries, applied to the tree head: the head is the log
|
|
53
|
+
// holder's OWN commitment. It proves ordering + append-only-ness only RELATIVE to itself; it does NOT
|
|
54
|
+
// prove "existed at / unaltered since date T" until a P-3 trust-root signs/timestamps the 32-byte head.
|
|
55
|
+
const SELF_ASSERTED_HEAD_NOTE =
|
|
56
|
+
"this tree head is SELF-ASSERTED (the log holder's own commitment to its journal as it stands now); " +
|
|
57
|
+
'it does NOT by itself prove "existed at / unaltered since date T" until a trust-root signs/timestamps the head (P-3)';
|
|
58
|
+
|
|
59
|
+
// What a check-proof ACCEPT does — and does NOT — mean: the proof verifies RELATIVE to the head embedded
|
|
60
|
+
// in the artifact. The auditor must compare that head against one they trust (e.g. a published/signed
|
|
61
|
+
// tree head) before relying on it; check-proof itself never sees the journal.
|
|
62
|
+
const CHECK_PROOF_NOTE =
|
|
63
|
+
"ACCEPTED means the proof verifies against the head EMBEDDED in the artifact; compare that head " +
|
|
64
|
+
"(size + root) against a tree head you trust (e.g. one the operator published/signed) before relying on it";
|
|
65
|
+
// ---------------------------------------------------------------------------------------------------
|
|
66
|
+
// buildVerifyBodyFromSeal — read an evidence-seal packet file from disk, load the bytes it REFERENCES,
|
|
67
|
+
// and construct the `verifyRequest` transport body. REUSES the existing evidence readers verbatim; the
|
|
68
|
+
// composed verdict comes from `verifyRequest` unchanged. Throws a JournalError on an unreadable/invalid
|
|
69
|
+
// packet (a caller/IO error), never a silent bad verdict.
|
|
70
|
+
// ---------------------------------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} artifactPath path to a *.vhevidence.json (unsigned seal) or *.vhevidence.json signed
|
|
74
|
+
* container. Resolved; the files it references resolve next to it (or --dir).
|
|
75
|
+
* @param {string|null} dir optional base dir for the referenced files (default: the packet's dir).
|
|
76
|
+
* @returns {{ body: object }} a `verifyRequest`-shaped body (kind verify-seal | verify-signed-seal).
|
|
77
|
+
*/
|
|
78
|
+
function buildVerifyBodyFromSeal(artifactPath, dir) {
|
|
79
|
+
const packetPath = path.resolve(artifactPath);
|
|
80
|
+
let text;
|
|
81
|
+
try {
|
|
82
|
+
text = fs.readFileSync(packetPath, "utf8");
|
|
83
|
+
} catch (e) {
|
|
84
|
+
throw new JournalError(`cannot read artifact ${artifactPath}: ${e.message}`);
|
|
85
|
+
}
|
|
86
|
+
let obj;
|
|
87
|
+
try {
|
|
88
|
+
obj = JSON.parse(text);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
throw new JournalError(`artifact ${artifactPath} is not valid JSON: ${e.message}`);
|
|
91
|
+
}
|
|
92
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
93
|
+
throw new JournalError(`artifact ${artifactPath} must be an evidence-seal packet object`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const signed = obj.kind === evidence.SIGNED_SEAL_KIND;
|
|
97
|
+
// For a signed container the sealed file list lives inside the embedded attestation; for an unsigned
|
|
98
|
+
// seal it is the top-level packet. readSeal STRICT-validates either shape (throws on a foreign/edited/
|
|
99
|
+
// wrong-kind packet), so we surface that as a clean JournalError rather than a bad verdict.
|
|
100
|
+
let seal;
|
|
101
|
+
try {
|
|
102
|
+
seal = signed ? evidence.readSeal(obj.attestation) : evidence.readSeal(obj);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
throw new JournalError(`invalid evidence packet ${artifactPath}: ${e.message}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const baseDir = dir != null ? path.resolve(dir) : path.dirname(packetPath);
|
|
108
|
+
const entries = [];
|
|
109
|
+
for (const f of seal.files) {
|
|
110
|
+
const abs = path.resolve(baseDir, f.relPath);
|
|
111
|
+
let bytes;
|
|
112
|
+
try {
|
|
113
|
+
bytes = fs.readFileSync(abs);
|
|
114
|
+
} catch (_) {
|
|
115
|
+
// Absent -> the verify core reports MISSING (a REJECTED content verdict), never an abort. We simply
|
|
116
|
+
// do not supply that entry; verifyRequest/verifySeal localizes it.
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
entries.push({ relPath: f.relPath, content: bytes.toString("base64"), encoding: "base64" });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (signed) {
|
|
123
|
+
// Bind the signed payload to OUR bytes: supplying `entries` makes verifyRequest recompute the canonical
|
|
124
|
+
// seal from them and require a byte-identical match to the signed payload (a drifted file is REJECTED).
|
|
125
|
+
return { body: { kind: "verify-signed-seal", container: obj, entries } };
|
|
126
|
+
}
|
|
127
|
+
return { body: { kind: "verify-seal", seal: obj, entries } };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------------------------------
|
|
131
|
+
// readJournalFile / lastEntry — parse a JSONL journal off disk into the entry array the pure core walks.
|
|
132
|
+
// A malformed line is surfaced as a JournalError naming the 1-based line number (never a silent skip).
|
|
133
|
+
// ---------------------------------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read + parse a JSONL journal file into an ordered entry array. A missing file is treated as an EMPTY
|
|
137
|
+
* journal (the first `append` creates it) — NOT an error. A present-but-unparseable line throws.
|
|
138
|
+
* @param {string} journalPath
|
|
139
|
+
* @returns {object[]} the parsed entries in file order (may be empty).
|
|
140
|
+
*/
|
|
141
|
+
function readJournalFile(journalPath) {
|
|
142
|
+
const abs = path.resolve(journalPath);
|
|
143
|
+
let raw;
|
|
144
|
+
try {
|
|
145
|
+
raw = fs.readFileSync(abs, "utf8");
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (e && e.code === "ENOENT") return []; // absent = empty journal
|
|
148
|
+
throw new JournalError(`cannot read journal ${journalPath}: ${e.message}`);
|
|
149
|
+
}
|
|
150
|
+
const entries = [];
|
|
151
|
+
const lines = raw.split("\n");
|
|
152
|
+
for (let i = 0; i < lines.length; i++) {
|
|
153
|
+
const line = lines[i];
|
|
154
|
+
if (line.trim() === "") continue; // tolerate a trailing newline / blank lines
|
|
155
|
+
let obj;
|
|
156
|
+
try {
|
|
157
|
+
obj = JSON.parse(line);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
throw new JournalError(`journal ${journalPath} line ${i + 1} is not valid JSON: ${e.message}`);
|
|
160
|
+
}
|
|
161
|
+
entries.push(obj);
|
|
162
|
+
}
|
|
163
|
+
return entries;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The last entry of a parsed journal, or null when empty (so appendEntry starts a genesis chain). */
|
|
167
|
+
function lastEntry(entries) {
|
|
168
|
+
return entries.length === 0 ? null : entries[entries.length - 1];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------------------------------
|
|
172
|
+
// runJournalAppend(opts, io) — verify <artifact> through the composed path, then append ONE entry line
|
|
173
|
+
// to the journal file STRICTLY ADDITIVELY. Exit 0 on a clean append (regardless of the recorded verdict —
|
|
174
|
+
// recording a REJECT is a successful append), 2 usage, 1 IO.
|
|
175
|
+
//
|
|
176
|
+
// NOTE ON the drift case: appending an observation whose verdict is REJECTED is itself a SUCCESSFUL append
|
|
177
|
+
// (exit 0) — the journal's job is to RECORD what it saw, tamper-evidently. The drift shows up later at
|
|
178
|
+
// `vh journal verify` time only if a PAST line was edited; a recorded REJECT is a faithful entry, not a
|
|
179
|
+
// broken chain. The `--json` verdict makes the recorded ACCEPTED/REJECTED machine-readable.
|
|
180
|
+
// ---------------------------------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {object} opts { artifact, to, dir?, vendor?, ts?, json? }
|
|
184
|
+
* - artifact (required) the seal packet to verify + record.
|
|
185
|
+
* - to (required) the journal file to append to (created if absent).
|
|
186
|
+
* - dir (optional) base dir for the seal's referenced files.
|
|
187
|
+
* - ts (optional) a SELF-ASSERTED timestamp to stamp the entry with (default: now, ISO).
|
|
188
|
+
* - json (optional) emit the machine verdict envelope to stdout.
|
|
189
|
+
* @param {object} io { write, writeErr }
|
|
190
|
+
* @returns {number} exit code (0 ok / 2 usage / 1 IO)
|
|
191
|
+
*/
|
|
192
|
+
function runJournalAppend(opts, io = {}) {
|
|
193
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
194
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
195
|
+
|
|
196
|
+
if (!opts.artifact) {
|
|
197
|
+
writeErr("error: `vh journal append` requires an <artifact> (a *.vhevidence.json seal)\n");
|
|
198
|
+
return JOURNAL_EXIT.USAGE;
|
|
199
|
+
}
|
|
200
|
+
if (!opts.to) {
|
|
201
|
+
writeErr("error: `vh journal append` requires --to <journalfile>\n");
|
|
202
|
+
return JOURNAL_EXIT.USAGE;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 1) Verify the artifact through the EXISTING composed verify path — the recorded verdict is byte-for-byte
|
|
206
|
+
// whatever verifyRequest returns (never re-derived here).
|
|
207
|
+
let body;
|
|
208
|
+
try {
|
|
209
|
+
({ body } = buildVerifyBodyFromSeal(opts.artifact, opts.dir != null ? opts.dir : null));
|
|
210
|
+
} catch (e) {
|
|
211
|
+
writeErr(`error: ${e.message}\n`);
|
|
212
|
+
return JOURNAL_EXIT.IO;
|
|
213
|
+
}
|
|
214
|
+
const verdict = verifyRequest(body);
|
|
215
|
+
|
|
216
|
+
// 2) Read the current journal (absent = empty), chain onto its last entry.
|
|
217
|
+
let existing;
|
|
218
|
+
try {
|
|
219
|
+
existing = readJournalFile(opts.to);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
writeErr(`error: ${e.message}\n`);
|
|
222
|
+
return JOURNAL_EXIT.IO;
|
|
223
|
+
}
|
|
224
|
+
// Guard: never append onto an ALREADY-broken chain (that would bury the break under a new line and make
|
|
225
|
+
// it look sound from the head). A broken existing journal is a clean IO/usage refusal, not an append.
|
|
226
|
+
if (existing.length > 0) {
|
|
227
|
+
const pre = verifyJournal(existing);
|
|
228
|
+
if (!pre.ok) {
|
|
229
|
+
writeErr(
|
|
230
|
+
`error: refusing to append — existing journal ${opts.to} is already broken at seq ${pre.brokenAt}: ${pre.reason}\n`
|
|
231
|
+
);
|
|
232
|
+
return JOURNAL_EXIT.IO;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const ts = opts.ts !== undefined && opts.ts !== null ? opts.ts : new Date().toISOString();
|
|
237
|
+
let entry;
|
|
238
|
+
try {
|
|
239
|
+
entry = appendEntry(lastEntry(existing), {
|
|
240
|
+
verdict,
|
|
241
|
+
artifact: opts.artifact,
|
|
242
|
+
ts,
|
|
243
|
+
});
|
|
244
|
+
} catch (e) {
|
|
245
|
+
writeErr(`error: could not build journal entry: ${e.message}\n`);
|
|
246
|
+
return JOURNAL_EXIT.IO;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 3) Append STRICTLY ADDITIVELY — one canonical JSON line, prior bytes untouched. We serialize the entry
|
|
250
|
+
// with the SAME recursive key-sorted encoder used for the hash, so the on-disk line re-parses to a
|
|
251
|
+
// value whose entryHash re-derives identically.
|
|
252
|
+
const line = canonicalize(entry) + "\n";
|
|
253
|
+
try {
|
|
254
|
+
fs.appendFileSync(path.resolve(opts.to), line, "utf8");
|
|
255
|
+
} catch (e) {
|
|
256
|
+
writeErr(`error: cannot append to journal ${opts.to}: ${e.message}\n`);
|
|
257
|
+
return JOURNAL_EXIT.IO;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (opts.json) {
|
|
261
|
+
write(
|
|
262
|
+
JSON.stringify(
|
|
263
|
+
{
|
|
264
|
+
appended: true,
|
|
265
|
+
journal: opts.to,
|
|
266
|
+
seq: entry.seq,
|
|
267
|
+
entryHash: entry.entryHash,
|
|
268
|
+
prevHash: entry.prevHash,
|
|
269
|
+
ts: entry.ts,
|
|
270
|
+
artifact: entry.artifact,
|
|
271
|
+
verdict: verdict.verdict, // ACCEPTED | REJECTED | ERROR — the recorded top-level answer
|
|
272
|
+
recorded: verdict, // the full composed verdict envelope, VERBATIM
|
|
273
|
+
},
|
|
274
|
+
null,
|
|
275
|
+
2
|
|
276
|
+
) + "\n"
|
|
277
|
+
);
|
|
278
|
+
} else {
|
|
279
|
+
write(
|
|
280
|
+
`appended seq ${entry.seq} to ${opts.to} — recorded verdict ${verdict.verdict} for ${opts.artifact}\n` +
|
|
281
|
+
` entryHash ${entry.entryHash}\n`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return JOURNAL_EXIT.OK;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------------------------------
|
|
288
|
+
// runJournalVerify(opts, io) — walk the on-disk chain through the pure core. Exit 0 on a sound chain
|
|
289
|
+
// (PASS), 3 on a broken chain (naming the drifted artifact + the seq where it drifted, and `brokenAt`),
|
|
290
|
+
// 2 usage, 1 IO. This is the SHARED 0/3 verify contract.
|
|
291
|
+
// ---------------------------------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @param {object} opts { journal, json? }
|
|
295
|
+
* @param {object} io { write, writeErr }
|
|
296
|
+
* @returns {number} exit code (0 PASS / 3 broken / 2 usage / 1 IO)
|
|
297
|
+
*/
|
|
298
|
+
function runJournalVerify(opts, io = {}) {
|
|
299
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
300
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
301
|
+
|
|
302
|
+
if (!opts.journal) {
|
|
303
|
+
writeErr("error: `vh journal verify` requires a <journalfile>\n");
|
|
304
|
+
return JOURNAL_EXIT.USAGE;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// A missing journal file is an IO error here (verify was asked to check a file that isn't there) — as
|
|
308
|
+
// opposed to append, where absent means "start a new one".
|
|
309
|
+
const abs = path.resolve(opts.journal);
|
|
310
|
+
if (!fs.existsSync(abs)) {
|
|
311
|
+
writeErr(`error: journal ${opts.journal} does not exist\n`);
|
|
312
|
+
return JOURNAL_EXIT.IO;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let entries;
|
|
316
|
+
try {
|
|
317
|
+
entries = readJournalFile(opts.journal);
|
|
318
|
+
} catch (e) {
|
|
319
|
+
// A malformed line means SOME past line was hand-edited into non-JSON — that is a tamper, reported on
|
|
320
|
+
// the shared drift exit (3) with a broken-chain verdict rather than a silent IO failure.
|
|
321
|
+
const verdict = { ok: false, brokenAt: null, reason: e.message, journal: opts.journal };
|
|
322
|
+
if (opts.json) {
|
|
323
|
+
write(JSON.stringify({ ...verdict, verdict: "BROKEN" }, null, 2) + "\n");
|
|
324
|
+
} else {
|
|
325
|
+
writeErr(`FAIL: journal ${opts.journal} is BROKEN — ${e.message}\n`);
|
|
326
|
+
}
|
|
327
|
+
return JOURNAL_EXIT.DRIFT;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const result = verifyJournal(entries);
|
|
331
|
+
|
|
332
|
+
// FAILURE MODE 1 — the hash-CHAIN itself is broken (a deleted / reordered / inserted / hand-edited past
|
|
333
|
+
// line). This takes precedence over content drift: a broken chain means we can no longer trust ANY of the
|
|
334
|
+
// recorded verdicts. Report the drifted artifact + the seq where it drifted, plus brokenAt (the index).
|
|
335
|
+
if (!result.ok) {
|
|
336
|
+
const brokenEntry = Number.isInteger(result.brokenAt) ? entries[result.brokenAt] : undefined;
|
|
337
|
+
const driftedArtifact =
|
|
338
|
+
brokenEntry && typeof brokenEntry === "object" && "artifact" in brokenEntry
|
|
339
|
+
? brokenEntry.artifact
|
|
340
|
+
: null;
|
|
341
|
+
const driftedSeq =
|
|
342
|
+
brokenEntry && typeof brokenEntry === "object" && Number.isInteger(brokenEntry.seq)
|
|
343
|
+
? brokenEntry.seq
|
|
344
|
+
: result.brokenAt;
|
|
345
|
+
|
|
346
|
+
if (opts.json) {
|
|
347
|
+
write(
|
|
348
|
+
JSON.stringify(
|
|
349
|
+
{
|
|
350
|
+
ok: false,
|
|
351
|
+
verdict: "BROKEN",
|
|
352
|
+
journal: opts.journal,
|
|
353
|
+
brokenAt: result.brokenAt,
|
|
354
|
+
seq: driftedSeq,
|
|
355
|
+
artifact: driftedArtifact,
|
|
356
|
+
reason: result.reason,
|
|
357
|
+
},
|
|
358
|
+
null,
|
|
359
|
+
2
|
|
360
|
+
) + "\n"
|
|
361
|
+
);
|
|
362
|
+
} else {
|
|
363
|
+
writeErr(
|
|
364
|
+
`FAIL: journal ${opts.journal} is BROKEN at seq ${driftedSeq}` +
|
|
365
|
+
(driftedArtifact != null ? ` (artifact ${JSON.stringify(driftedArtifact)})` : "") +
|
|
366
|
+
` — ${result.reason}\n` +
|
|
367
|
+
` brokenAt index ${result.brokenAt}\n`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return JOURNAL_EXIT.DRIFT;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// FAILURE MODE 2 — the chain is INTACT (every recorded observation is authentic + in order) but SOME
|
|
374
|
+
// recorded observation is itself a DRIFT: its verdict is not ACCEPTED. This is the "integrity OVER TIME"
|
|
375
|
+
// signal — the artifact was verified continuously and one observation FAILED. We report the FIRST such
|
|
376
|
+
// entry: the drifted artifact + the seq where it drifted. A one-shot verify cannot produce this; the
|
|
377
|
+
// journal can, because it recorded every observation tamper-evidently.
|
|
378
|
+
const drift = firstRecordedDrift(entries);
|
|
379
|
+
if (drift) {
|
|
380
|
+
if (opts.json) {
|
|
381
|
+
write(
|
|
382
|
+
JSON.stringify(
|
|
383
|
+
{
|
|
384
|
+
ok: false,
|
|
385
|
+
verdict: "DRIFTED",
|
|
386
|
+
journal: opts.journal,
|
|
387
|
+
count: result.count,
|
|
388
|
+
head: result.head,
|
|
389
|
+
// The chain is sound, so brokenAt is null — nothing was TAMPERED; an observation just FAILED.
|
|
390
|
+
brokenAt: null,
|
|
391
|
+
seq: drift.seq,
|
|
392
|
+
artifact: drift.artifact,
|
|
393
|
+
recordedVerdict: drift.verdict,
|
|
394
|
+
reason: `entry seq ${drift.seq} recorded a ${drift.verdict} verdict for ${JSON.stringify(drift.artifact)}`,
|
|
395
|
+
},
|
|
396
|
+
null,
|
|
397
|
+
2
|
|
398
|
+
) + "\n"
|
|
399
|
+
);
|
|
400
|
+
} else {
|
|
401
|
+
writeErr(
|
|
402
|
+
`FAIL: journal ${opts.journal} recorded DRIFT at seq ${drift.seq}` +
|
|
403
|
+
(drift.artifact != null ? ` (artifact ${JSON.stringify(drift.artifact)})` : "") +
|
|
404
|
+
` — recorded verdict ${drift.verdict} (the chain is intact; an observation FAILED)\n`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return JOURNAL_EXIT.DRIFT;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// PASS — the chain is unbroken AND every recorded observation was ACCEPTED: continuous integrity from the
|
|
411
|
+
// first entry to the head.
|
|
412
|
+
if (opts.json) {
|
|
413
|
+
write(
|
|
414
|
+
JSON.stringify(
|
|
415
|
+
{ ok: true, verdict: "PASS", journal: opts.journal, count: result.count, head: result.head },
|
|
416
|
+
null,
|
|
417
|
+
2
|
|
418
|
+
) + "\n"
|
|
419
|
+
);
|
|
420
|
+
} else {
|
|
421
|
+
write(
|
|
422
|
+
`PASS: journal ${opts.journal} is unbroken — ${result.count} ` +
|
|
423
|
+
`entr${result.count === 1 ? "y" : "ies"} chain to head ${result.head}, every observation ACCEPTED\n`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return JOURNAL_EXIT.OK;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Scan an INTACT (already chain-verified) journal for the FIRST entry whose recorded verdict is not
|
|
430
|
+
// ACCEPTED. Returns { seq, artifact, verdict } for that entry, or null when every observation was ACCEPTED.
|
|
431
|
+
// The recorded verdict lives at `entry.verdict.verdict` (the composed envelope's top-level answer); a
|
|
432
|
+
// missing/mis-shaped verdict envelope is itself treated as a drift (fail closed — never a silent PASS).
|
|
433
|
+
function firstRecordedDrift(entries) {
|
|
434
|
+
for (const e of entries) {
|
|
435
|
+
const v = e && typeof e === "object" ? e.verdict : undefined;
|
|
436
|
+
const answer = v && typeof v === "object" ? v.verdict : undefined;
|
|
437
|
+
if (answer !== VERDICT.ACCEPTED) {
|
|
438
|
+
return { seq: e.seq, artifact: e.artifact === undefined ? null : e.artifact, verdict: answer === undefined ? "MALFORMED" : answer };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---------------------------------------------------------------------------------------------------
|
|
445
|
+
// Transparency-log helpers (T-63.2): the ordered LEAVES the Merkle log commits to are the journal's
|
|
446
|
+
// entry hashes IN FILE ORDER, and every log-shaped command refuses to operate over a broken chain —
|
|
447
|
+
// a head/proof over a tampered journal would be a false attestation, so it fails CLOSED on exit 3.
|
|
448
|
+
// ---------------------------------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
/** The ordered Merkle-log leaves of a parsed journal: its entry hashes, in file order. */
|
|
451
|
+
function entryLeaves(entries) {
|
|
452
|
+
return entries.map((e) => e.entryHash);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Load + chain-verify a journal for the tree-head/prove-* commands.
|
|
456
|
+
// Returns { ok:true, entries } or { ok:false, code, io?, reason, brokenAt } — the caller formats output.
|
|
457
|
+
function loadIntactEntries(journalPath) {
|
|
458
|
+
const abs = path.resolve(journalPath);
|
|
459
|
+
if (!fs.existsSync(abs)) {
|
|
460
|
+
return { ok: false, code: JOURNAL_EXIT.IO, io: true, reason: `journal ${journalPath} does not exist` };
|
|
461
|
+
}
|
|
462
|
+
let entries;
|
|
463
|
+
try {
|
|
464
|
+
entries = readJournalFile(journalPath);
|
|
465
|
+
} catch (e) {
|
|
466
|
+
// A non-JSON line means a past line was hand-edited — a tamper, on the shared drift exit (3).
|
|
467
|
+
return { ok: false, code: JOURNAL_EXIT.DRIFT, reason: e.message, brokenAt: null };
|
|
468
|
+
}
|
|
469
|
+
const result = verifyJournal(entries);
|
|
470
|
+
if (!result.ok) {
|
|
471
|
+
return { ok: false, code: JOURNAL_EXIT.DRIFT, reason: result.reason, brokenAt: result.brokenAt };
|
|
472
|
+
}
|
|
473
|
+
return { ok: true, entries };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Shared failure emitter for the load-and-verify preamble of tree-head/prove-*: an absent journal is a
|
|
477
|
+
// plain IO error; a broken chain is a BROKEN verdict on exit 3 (JSON when asked) that REFUSES the verb.
|
|
478
|
+
function emitLoadFailure(verbLabel, journal, res, opts, io) {
|
|
479
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
480
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
481
|
+
if (res.io) {
|
|
482
|
+
writeErr(`error: ${res.reason}\n`);
|
|
483
|
+
return res.code;
|
|
484
|
+
}
|
|
485
|
+
if (opts.json) {
|
|
486
|
+
write(
|
|
487
|
+
JSON.stringify(
|
|
488
|
+
{
|
|
489
|
+
ok: false,
|
|
490
|
+
verdict: "BROKEN",
|
|
491
|
+
journal,
|
|
492
|
+
brokenAt: res.brokenAt === undefined ? null : res.brokenAt,
|
|
493
|
+
reason: `refusing to ${verbLabel} over a broken chain: ${res.reason}`,
|
|
494
|
+
},
|
|
495
|
+
null,
|
|
496
|
+
2
|
|
497
|
+
) + "\n"
|
|
498
|
+
);
|
|
499
|
+
} else {
|
|
500
|
+
writeErr(
|
|
501
|
+
`FAIL: journal ${journal} is BROKEN — refusing to ${verbLabel} over a broken chain (${res.reason})\n`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
return res.code;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Write a proof artifact to a caller-named path (pretty JSON + trailing newline). Throws on IO failure.
|
|
508
|
+
function writeProofArtifact(artifact, outPath) {
|
|
509
|
+
fs.writeFileSync(path.resolve(outPath), JSON.stringify(artifact, null, 2) + "\n", "utf8");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ---------------------------------------------------------------------------------------------------
|
|
513
|
+
// runJournalTreeHead(opts, io) — print the publishable Signed-Tree-Head-SHAPED commitment { size, root }
|
|
514
|
+
// over the journal's ordered entry hashes. Read-only; carries the self-asserted-head honesty note.
|
|
515
|
+
// Exit 0 head printed / 3 broken chain / 2 usage / 1 IO.
|
|
516
|
+
// ---------------------------------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @param {object} opts { journal, json? }
|
|
520
|
+
* @param {object} io { write, writeErr }
|
|
521
|
+
* @returns {number} exit code
|
|
522
|
+
*/
|
|
523
|
+
function runJournalTreeHead(opts, io = {}) {
|
|
524
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
525
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
526
|
+
|
|
527
|
+
if (!opts.journal) {
|
|
528
|
+
writeErr("error: `vh journal tree-head` requires a <journalfile>\n");
|
|
529
|
+
return JOURNAL_EXIT.USAGE;
|
|
530
|
+
}
|
|
531
|
+
const res = loadIntactEntries(opts.journal);
|
|
532
|
+
if (!res.ok) return emitLoadFailure("compute a tree head", opts.journal, res, opts, io);
|
|
533
|
+
|
|
534
|
+
const head = journalLog.treeHead(entryLeaves(res.entries));
|
|
535
|
+
if (head.root === null) {
|
|
536
|
+
// Unreachable over an intact chain (verifyJournal validated every entryHash), but fail CLOSED.
|
|
537
|
+
writeErr(`error: journal ${opts.journal} yielded malformed entry hashes — no head computed\n`);
|
|
538
|
+
return JOURNAL_EXIT.DRIFT;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (opts.json) {
|
|
542
|
+
write(
|
|
543
|
+
JSON.stringify(
|
|
544
|
+
{
|
|
545
|
+
ok: true,
|
|
546
|
+
verdict: "HEAD",
|
|
547
|
+
journal: opts.journal,
|
|
548
|
+
size: head.size,
|
|
549
|
+
root: head.root,
|
|
550
|
+
note: SELF_ASSERTED_HEAD_NOTE,
|
|
551
|
+
},
|
|
552
|
+
null,
|
|
553
|
+
2
|
|
554
|
+
) + "\n"
|
|
555
|
+
);
|
|
556
|
+
} else {
|
|
557
|
+
write(
|
|
558
|
+
`tree head of ${opts.journal}: { size: ${head.size}, root: ${head.root} }\n` +
|
|
559
|
+
`NOTE: ${SELF_ASSERTED_HEAD_NOTE}\n`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return JOURNAL_EXIT.OK;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------------------------------------------------
|
|
566
|
+
// runJournalProveInclusion(opts, io) — emit a compact, SELF-CONTAINED inclusion-proof artifact
|
|
567
|
+
// { kind:"vh-journal-inclusion", leaf, seq, size, root, path[] } for the entry at --seq. Read-only
|
|
568
|
+
// except the --out file the caller names. Exit 0 proved / 3 broken chain / 2 usage / 1 IO.
|
|
569
|
+
// ---------------------------------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* @param {object} opts { journal, seq, out?, json? }
|
|
573
|
+
* @param {object} io { write, writeErr }
|
|
574
|
+
* @returns {number} exit code
|
|
575
|
+
*/
|
|
576
|
+
function runJournalProveInclusion(opts, io = {}) {
|
|
577
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
578
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
579
|
+
|
|
580
|
+
if (!opts.journal) {
|
|
581
|
+
writeErr("error: `vh journal prove-inclusion` requires a <journalfile>\n");
|
|
582
|
+
return JOURNAL_EXIT.USAGE;
|
|
583
|
+
}
|
|
584
|
+
if (opts.seq === undefined) {
|
|
585
|
+
writeErr("error: `vh journal prove-inclusion` requires --seq <i> (the entry to prove)\n");
|
|
586
|
+
return JOURNAL_EXIT.USAGE;
|
|
587
|
+
}
|
|
588
|
+
if (!/^\d+$/.test(String(opts.seq))) {
|
|
589
|
+
writeErr(`error: --seq must be a non-negative integer, got ${JSON.stringify(opts.seq)}\n`);
|
|
590
|
+
return JOURNAL_EXIT.USAGE;
|
|
591
|
+
}
|
|
592
|
+
const seq = Number(opts.seq);
|
|
593
|
+
|
|
594
|
+
const res = loadIntactEntries(opts.journal);
|
|
595
|
+
if (!res.ok) return emitLoadFailure("prove inclusion", opts.journal, res, opts, io);
|
|
596
|
+
|
|
597
|
+
const leaves = entryLeaves(res.entries);
|
|
598
|
+
if (leaves.length === 0) {
|
|
599
|
+
writeErr(`error: journal ${opts.journal} has 0 entries — nothing to prove inclusion of\n`);
|
|
600
|
+
return JOURNAL_EXIT.USAGE;
|
|
601
|
+
}
|
|
602
|
+
if (seq >= leaves.length) {
|
|
603
|
+
writeErr(
|
|
604
|
+
`error: --seq ${seq} is out of range — journal ${opts.journal} has ${leaves.length} ` +
|
|
605
|
+
`entr${leaves.length === 1 ? "y" : "ies"} (valid seq: 0..${leaves.length - 1})\n`
|
|
606
|
+
);
|
|
607
|
+
return JOURNAL_EXIT.USAGE;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const head = journalLog.treeHead(leaves);
|
|
611
|
+
const proof = journalLog.inclusionProof(leaves, seq);
|
|
612
|
+
if (head.root === null || proof === null) {
|
|
613
|
+
writeErr(`error: journal ${opts.journal} yielded malformed entry hashes — no proof emitted\n`);
|
|
614
|
+
return JOURNAL_EXIT.DRIFT;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// The self-contained, documented artifact: everything check-proof needs, and NOTHING of the log itself.
|
|
618
|
+
const artifact = {
|
|
619
|
+
kind: JOURNAL_INCLUSION_PROOF_KIND,
|
|
620
|
+
journal: opts.journal,
|
|
621
|
+
leaf: proof.leaf,
|
|
622
|
+
seq,
|
|
623
|
+
size: head.size,
|
|
624
|
+
root: head.root,
|
|
625
|
+
path: proof.path,
|
|
626
|
+
note: SELF_ASSERTED_HEAD_NOTE,
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
if (opts.out) {
|
|
630
|
+
try {
|
|
631
|
+
writeProofArtifact(artifact, opts.out);
|
|
632
|
+
} catch (e) {
|
|
633
|
+
writeErr(`error: cannot write proof to ${opts.out}: ${e.message}\n`);
|
|
634
|
+
return JOURNAL_EXIT.IO;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (opts.json) {
|
|
639
|
+
write(
|
|
640
|
+
JSON.stringify(
|
|
641
|
+
{
|
|
642
|
+
ok: true,
|
|
643
|
+
verdict: "PROVED",
|
|
644
|
+
kind: JOURNAL_INCLUSION_PROOF_KIND,
|
|
645
|
+
journal: opts.journal,
|
|
646
|
+
seq,
|
|
647
|
+
size: head.size,
|
|
648
|
+
root: head.root,
|
|
649
|
+
out: opts.out || null,
|
|
650
|
+
artifact,
|
|
651
|
+
},
|
|
652
|
+
null,
|
|
653
|
+
2
|
|
654
|
+
) + "\n"
|
|
655
|
+
);
|
|
656
|
+
} else if (opts.out) {
|
|
657
|
+
write(
|
|
658
|
+
`wrote inclusion proof for seq ${seq} of ${opts.journal} to ${opts.out}\n` +
|
|
659
|
+
` head { size: ${head.size}, root: ${head.root} }\n` +
|
|
660
|
+
` NOTE: ${SELF_ASSERTED_HEAD_NOTE}\n`
|
|
661
|
+
);
|
|
662
|
+
} else {
|
|
663
|
+
write(JSON.stringify(artifact, null, 2) + "\n");
|
|
664
|
+
}
|
|
665
|
+
return JOURNAL_EXIT.OK;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ---------------------------------------------------------------------------------------------------
|
|
669
|
+
// runJournalProveConsistency(opts, io) — emit a SELF-CONTAINED consistency-proof artifact
|
|
670
|
+
// { kind:"vh-journal-consistency", first:{size:m,root}, second:{size:n,root}, proof[] } proving the
|
|
671
|
+
// current size-n log is an APPEND-ONLY extension of its size---from prefix. Read-only except --out.
|
|
672
|
+
// Exit 0 proved / 3 broken chain / 2 usage / 1 IO.
|
|
673
|
+
// ---------------------------------------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* @param {object} opts { journal, from, out?, json? }
|
|
677
|
+
* @param {object} io { write, writeErr }
|
|
678
|
+
* @returns {number} exit code
|
|
679
|
+
*/
|
|
680
|
+
function runJournalProveConsistency(opts, io = {}) {
|
|
681
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
682
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
683
|
+
|
|
684
|
+
if (!opts.journal) {
|
|
685
|
+
writeErr("error: `vh journal prove-consistency` requires a <journalfile>\n");
|
|
686
|
+
return JOURNAL_EXIT.USAGE;
|
|
687
|
+
}
|
|
688
|
+
if (opts.from === undefined) {
|
|
689
|
+
writeErr("error: `vh journal prove-consistency` requires --from <oldSize> (the older tree size)\n");
|
|
690
|
+
return JOURNAL_EXIT.USAGE;
|
|
691
|
+
}
|
|
692
|
+
if (!/^\d+$/.test(String(opts.from)) || Number(opts.from) < 1) {
|
|
693
|
+
writeErr(`error: --from must be an integer >= 1, got ${JSON.stringify(opts.from)}\n`);
|
|
694
|
+
return JOURNAL_EXIT.USAGE;
|
|
695
|
+
}
|
|
696
|
+
const m = Number(opts.from);
|
|
697
|
+
|
|
698
|
+
const res = loadIntactEntries(opts.journal);
|
|
699
|
+
if (!res.ok) return emitLoadFailure("prove consistency", opts.journal, res, opts, io);
|
|
700
|
+
|
|
701
|
+
const leaves = entryLeaves(res.entries);
|
|
702
|
+
const n = leaves.length;
|
|
703
|
+
if (m > n) {
|
|
704
|
+
writeErr(
|
|
705
|
+
`error: --from ${m} is out of range — journal ${opts.journal} has ${n} ` +
|
|
706
|
+
`entr${n === 1 ? "y" : "ies"} (valid --from: 1..${n})\n`
|
|
707
|
+
);
|
|
708
|
+
return JOURNAL_EXIT.USAGE;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const firstHead = journalLog.treeHead(leaves.slice(0, m));
|
|
712
|
+
const secondHead = journalLog.treeHead(leaves);
|
|
713
|
+
const proof = journalLog.consistencyProof(leaves, m, n);
|
|
714
|
+
if (firstHead.root === null || secondHead.root === null || proof === null) {
|
|
715
|
+
writeErr(`error: journal ${opts.journal} yielded malformed entry hashes — no proof emitted\n`);
|
|
716
|
+
return JOURNAL_EXIT.DRIFT;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const artifact = {
|
|
720
|
+
kind: JOURNAL_CONSISTENCY_PROOF_KIND,
|
|
721
|
+
journal: opts.journal,
|
|
722
|
+
first: { size: m, root: firstHead.root },
|
|
723
|
+
second: { size: n, root: secondHead.root },
|
|
724
|
+
proof: proof.path,
|
|
725
|
+
note: SELF_ASSERTED_HEAD_NOTE,
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
if (opts.out) {
|
|
729
|
+
try {
|
|
730
|
+
writeProofArtifact(artifact, opts.out);
|
|
731
|
+
} catch (e) {
|
|
732
|
+
writeErr(`error: cannot write proof to ${opts.out}: ${e.message}\n`);
|
|
733
|
+
return JOURNAL_EXIT.IO;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (opts.json) {
|
|
738
|
+
write(
|
|
739
|
+
JSON.stringify(
|
|
740
|
+
{
|
|
741
|
+
ok: true,
|
|
742
|
+
verdict: "PROVED",
|
|
743
|
+
kind: JOURNAL_CONSISTENCY_PROOF_KIND,
|
|
744
|
+
journal: opts.journal,
|
|
745
|
+
first: artifact.first,
|
|
746
|
+
second: artifact.second,
|
|
747
|
+
out: opts.out || null,
|
|
748
|
+
artifact,
|
|
749
|
+
},
|
|
750
|
+
null,
|
|
751
|
+
2
|
|
752
|
+
) + "\n"
|
|
753
|
+
);
|
|
754
|
+
} else if (opts.out) {
|
|
755
|
+
write(
|
|
756
|
+
`wrote consistency proof for ${opts.journal} to ${opts.out}\n` +
|
|
757
|
+
` first { size: ${m}, root: ${firstHead.root} }\n` +
|
|
758
|
+
` second { size: ${n}, root: ${secondHead.root} }\n` +
|
|
759
|
+
` NOTE: ${SELF_ASSERTED_HEAD_NOTE}\n`
|
|
760
|
+
);
|
|
761
|
+
} else {
|
|
762
|
+
write(JSON.stringify(artifact, null, 2) + "\n");
|
|
763
|
+
}
|
|
764
|
+
return JOURNAL_EXIT.OK;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ---------------------------------------------------------------------------------------------------
|
|
768
|
+
// runJournalCheckProof(opts, io) — the OFFLINE third-party AUDITOR command. Reads ONLY the proof
|
|
769
|
+
// artifact (NO journal, NO key, NO network) and calls verifyInclusion / verifyConsistency for the
|
|
770
|
+
// artifact's kind. ACCEPTED (exit 0) iff the proof verifies; REJECTED (exit 3) on ANY tamper, forge,
|
|
771
|
+
// unknown kind, or malformed artifact — fail CLOSED, never a silent pass. 2 usage / 1 IO.
|
|
772
|
+
// ---------------------------------------------------------------------------------------------------
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* @param {object} opts { proof, json? }
|
|
776
|
+
* @param {object} io { write, writeErr }
|
|
777
|
+
* @returns {number} exit code (0 ACCEPTED / 3 REJECTED / 2 usage / 1 IO)
|
|
778
|
+
*/
|
|
779
|
+
function runJournalCheckProof(opts, io = {}) {
|
|
780
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
781
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
782
|
+
|
|
783
|
+
if (!opts.proof) {
|
|
784
|
+
writeErr("error: `vh journal check-proof` requires a <prooffile>\n");
|
|
785
|
+
return JOURNAL_EXIT.USAGE;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// NOTE this function touches EXACTLY ONE file: the proof artifact. It never opens the journal (it has
|
|
789
|
+
// no idea where the journal is) and never opens a socket — a test runs it with NO journal present
|
|
790
|
+
// under a guard that trips on any journal/fs-path or network access.
|
|
791
|
+
let text;
|
|
792
|
+
try {
|
|
793
|
+
text = fs.readFileSync(path.resolve(opts.proof), "utf8");
|
|
794
|
+
} catch (e) {
|
|
795
|
+
writeErr(`error: cannot read proof ${opts.proof}: ${e.message}\n`);
|
|
796
|
+
return JOURNAL_EXIT.IO;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const reject = (reason, extra = {}) => {
|
|
800
|
+
if (opts.json) {
|
|
801
|
+
write(
|
|
802
|
+
JSON.stringify({ ok: false, verdict: "REJECTED", proof: opts.proof, reason, ...extra }, null, 2) + "\n"
|
|
803
|
+
);
|
|
804
|
+
} else {
|
|
805
|
+
writeErr(`REJECTED: proof ${opts.proof} — ${reason}\n`);
|
|
806
|
+
}
|
|
807
|
+
return JOURNAL_EXIT.DRIFT;
|
|
808
|
+
};
|
|
809
|
+
const accept = (summary, extra = {}) => {
|
|
810
|
+
if (opts.json) {
|
|
811
|
+
write(
|
|
812
|
+
JSON.stringify(
|
|
813
|
+
{ ok: true, verdict: "ACCEPTED", proof: opts.proof, ...extra, note: CHECK_PROOF_NOTE },
|
|
814
|
+
null,
|
|
815
|
+
2
|
|
816
|
+
) + "\n"
|
|
817
|
+
);
|
|
818
|
+
} else {
|
|
819
|
+
write(`ACCEPTED: ${summary}\n NOTE: ${CHECK_PROOF_NOTE}\n`);
|
|
820
|
+
}
|
|
821
|
+
return JOURNAL_EXIT.OK;
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
let artifact;
|
|
825
|
+
try {
|
|
826
|
+
artifact = JSON.parse(text);
|
|
827
|
+
} catch (e) {
|
|
828
|
+
// A proof file that does not even parse is a tampered/foreign artifact — REJECTED, never an accept.
|
|
829
|
+
return reject(`not valid JSON: ${e.message}`);
|
|
830
|
+
}
|
|
831
|
+
if (artifact === null || typeof artifact !== "object" || Array.isArray(artifact)) {
|
|
832
|
+
return reject("proof artifact must be a JSON object");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (artifact.kind === JOURNAL_INCLUSION_PROOF_KIND) {
|
|
836
|
+
// Rebuild the exact core-shaped proof + head from the self-contained artifact. verifyInclusion is
|
|
837
|
+
// TOTAL on hostile input (returns false, never throws), so a mangled field is a clean REJECT.
|
|
838
|
+
const ok = journalLog.verifyInclusion(
|
|
839
|
+
{ leaf: artifact.leaf, leafIndex: artifact.seq, treeSize: artifact.size, path: artifact.path },
|
|
840
|
+
{ size: artifact.size, root: artifact.root }
|
|
841
|
+
);
|
|
842
|
+
const detail = { kind: artifact.kind, leaf: artifact.leaf, seq: artifact.seq, size: artifact.size, root: artifact.root };
|
|
843
|
+
if (!ok) {
|
|
844
|
+
return reject(
|
|
845
|
+
"inclusion proof does NOT verify against its own head — a byte of leaf/root/path/seq/size was edited, or the proof is forged",
|
|
846
|
+
detail
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return accept(
|
|
850
|
+
`inclusion proof verifies — leaf ${artifact.leaf} is entry seq ${artifact.seq} under head { size: ${artifact.size}, root: ${artifact.root} }`,
|
|
851
|
+
detail
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (artifact.kind === JOURNAL_CONSISTENCY_PROOF_KIND) {
|
|
856
|
+
const first = artifact.first;
|
|
857
|
+
const second = artifact.second;
|
|
858
|
+
const ok = journalLog.verifyConsistency(
|
|
859
|
+
{
|
|
860
|
+
firstSize: first !== null && typeof first === "object" ? first.size : undefined,
|
|
861
|
+
secondSize: second !== null && typeof second === "object" ? second.size : undefined,
|
|
862
|
+
path: artifact.proof,
|
|
863
|
+
},
|
|
864
|
+
first,
|
|
865
|
+
second
|
|
866
|
+
);
|
|
867
|
+
const detail = { kind: artifact.kind, first, second };
|
|
868
|
+
if (!ok) {
|
|
869
|
+
return reject(
|
|
870
|
+
"consistency proof does NOT verify — the second head is NOT an append-only extension of the first (a past entry was rewritten, or the proof was edited/forged)",
|
|
871
|
+
detail
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return accept(
|
|
875
|
+
`consistency proof verifies — head { size: ${second && second.size}, root: ${second && second.root} } is an append-only extension of head { size: ${first && first.size}, root: ${first && first.root} }`,
|
|
876
|
+
detail
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return reject(
|
|
881
|
+
`unknown proof kind ${JSON.stringify(artifact.kind)} (expected ${JSON.stringify(JOURNAL_INCLUSION_PROOF_KIND)} or ${JSON.stringify(JOURNAL_CONSISTENCY_PROOF_KIND)})`,
|
|
882
|
+
{ kind: artifact.kind === undefined ? null : artifact.kind }
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// ---------------------------------------------------------------------------------------------------
|
|
887
|
+
// Argument parsing + the `vh journal` dispatcher.
|
|
888
|
+
// ---------------------------------------------------------------------------------------------------
|
|
889
|
+
|
|
890
|
+
/** Parse `journal append` argv into { artifact, to, dir, vendor, ts, json }. Throws on a bad flag. */
|
|
891
|
+
function parseAppendArgs(argv) {
|
|
892
|
+
const opts = { artifact: undefined, to: undefined, dir: undefined, vendor: undefined, ts: undefined, json: false };
|
|
893
|
+
for (let i = 0; i < argv.length; i++) {
|
|
894
|
+
const a = argv[i];
|
|
895
|
+
switch (a) {
|
|
896
|
+
case "--json":
|
|
897
|
+
opts.json = true;
|
|
898
|
+
break;
|
|
899
|
+
case "--to":
|
|
900
|
+
opts.to = argv[++i];
|
|
901
|
+
if (opts.to === undefined) throw new Error("--to requires a value");
|
|
902
|
+
break;
|
|
903
|
+
case "--dir":
|
|
904
|
+
opts.dir = argv[++i];
|
|
905
|
+
if (opts.dir === undefined) throw new Error("--dir requires a value");
|
|
906
|
+
break;
|
|
907
|
+
case "--vendor":
|
|
908
|
+
opts.vendor = argv[++i];
|
|
909
|
+
if (opts.vendor === undefined) throw new Error("--vendor requires a value");
|
|
910
|
+
break;
|
|
911
|
+
case "--ts":
|
|
912
|
+
opts.ts = argv[++i];
|
|
913
|
+
if (opts.ts === undefined) throw new Error("--ts requires a value");
|
|
914
|
+
break;
|
|
915
|
+
default:
|
|
916
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
917
|
+
if (opts.artifact !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
918
|
+
opts.artifact = a;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return opts;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** Parse `journal verify` argv into { journal, json }. Throws on a bad flag. */
|
|
925
|
+
function parseJournalVerifyArgs(argv) {
|
|
926
|
+
const opts = { journal: undefined, json: false };
|
|
927
|
+
for (let i = 0; i < argv.length; i++) {
|
|
928
|
+
const a = argv[i];
|
|
929
|
+
switch (a) {
|
|
930
|
+
case "--json":
|
|
931
|
+
opts.json = true;
|
|
932
|
+
break;
|
|
933
|
+
default:
|
|
934
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
935
|
+
if (opts.journal !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
936
|
+
opts.journal = a;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return opts;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Shared shape for the single-positional-plus-flags T-63.2 verbs. `positional` names the slot for error
|
|
943
|
+
// messages; `valueFlags` maps a flag (e.g. "--seq") to the opts key it fills. Throws on a bad flag.
|
|
944
|
+
function _parsePositionalWithFlags(argv, positional, valueFlags) {
|
|
945
|
+
const opts = { [positional]: undefined, json: false };
|
|
946
|
+
for (const key of Object.values(valueFlags)) opts[key] = undefined;
|
|
947
|
+
for (let i = 0; i < argv.length; i++) {
|
|
948
|
+
const a = argv[i];
|
|
949
|
+
if (a === "--json") {
|
|
950
|
+
opts.json = true;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (Object.prototype.hasOwnProperty.call(valueFlags, a)) {
|
|
954
|
+
const key = valueFlags[a];
|
|
955
|
+
opts[key] = argv[++i];
|
|
956
|
+
if (opts[key] === undefined) throw new Error(`${a} requires a value`);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
|
|
960
|
+
if (opts[positional] !== undefined) throw new Error(`unexpected extra argument: ${a}`);
|
|
961
|
+
opts[positional] = a;
|
|
962
|
+
}
|
|
963
|
+
return opts;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** Parse `journal tree-head` argv into { journal, json }. Throws on a bad flag. */
|
|
967
|
+
function parseTreeHeadArgs(argv) {
|
|
968
|
+
return _parsePositionalWithFlags(argv, "journal", {});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Parse `journal prove-inclusion` argv into { journal, seq, out, json }. Throws on a bad flag. */
|
|
972
|
+
function parseProveInclusionArgs(argv) {
|
|
973
|
+
return _parsePositionalWithFlags(argv, "journal", { "--seq": "seq", "--out": "out" });
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Parse `journal prove-consistency` argv into { journal, from, out, json }. Throws on a bad flag. */
|
|
977
|
+
function parseProveConsistencyArgs(argv) {
|
|
978
|
+
return _parsePositionalWithFlags(argv, "journal", { "--from": "from", "--out": "out" });
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** Parse `journal check-proof` argv into { proof, json }. Throws on a bad flag. */
|
|
982
|
+
function parseCheckProofArgs(argv) {
|
|
983
|
+
return _parsePositionalWithFlags(argv, "proof", {});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function journalUsage() {
|
|
987
|
+
return [
|
|
988
|
+
"vh journal — an APPEND-ONLY, HASH-CHAINED integrity journal of verify verdicts (integrity OVER TIME)",
|
|
989
|
+
"",
|
|
990
|
+
"Usage:",
|
|
991
|
+
" vh journal append <artifact> --to <journalfile> [--dir <d>] [--ts <ISO>] [--json]",
|
|
992
|
+
" vh journal verify <journalfile> [--json]",
|
|
993
|
+
" vh journal tree-head <journalfile> [--json]",
|
|
994
|
+
" vh journal prove-inclusion <journalfile> --seq <i> [--out <f>] [--json]",
|
|
995
|
+
" vh journal prove-consistency <journalfile> --from <oldSize> [--out <f>] [--json]",
|
|
996
|
+
" vh journal check-proof <prooffile> [--json]",
|
|
997
|
+
"",
|
|
998
|
+
"append VERIFIES <artifact> (a *.vhevidence.json seal / signed container) through the EXISTING composed",
|
|
999
|
+
" verify path and records the verdict as ONE new, hash-chained line — STRICTLY ADDITIVELY (prior lines",
|
|
1000
|
+
" are never rewritten). Recording a REJECTED verdict is a successful append; the journal's job is to",
|
|
1001
|
+
" faithfully record what it saw. Exit: 0 appended / 2 usage / 1 IO.",
|
|
1002
|
+
"verify walks the on-disk chain: a deleted / reordered / inserted / hand-edited past line BREAKS the chain",
|
|
1003
|
+
" and it LOCALIZES the first break — naming the drifted artifact + the seq where it drifted + brokenAt.",
|
|
1004
|
+
" Exit: 0 PASS (unbroken) / 3 BROKEN / 2 usage / 1 IO — the SHARED 0/3 verify contract.",
|
|
1005
|
+
"tree-head prints the publishable Signed-Tree-Head-SHAPED commitment { size, root } — the RFC-6962",
|
|
1006
|
+
" ordered Merkle head over the journal's entry hashes. The head is SELF-ASSERTED until a trust-root",
|
|
1007
|
+
" signs/timestamps it. Read-only. Exit: 0 head / 3 broken chain / 2 usage / 1 IO.",
|
|
1008
|
+
"prove-inclusion emits a compact, SELF-CONTAINED artifact { kind:\"vh-journal-inclusion\", leaf, seq,",
|
|
1009
|
+
" size, root, path[] } proving entry --seq is committed under the current head. Read-only (only the",
|
|
1010
|
+
" --out file is written). Exit: 0 proved / 3 broken chain / 2 usage / 1 IO.",
|
|
1011
|
+
"prove-consistency emits { kind:\"vh-journal-consistency\", first:{size,root}, second:{size,root},",
|
|
1012
|
+
" proof[] } proving the current log is an APPEND-ONLY extension of its size---from prefix — the",
|
|
1013
|
+
" \"no history was rewritten\" guarantee, compact. Exit: 0 proved / 3 broken chain / 2 usage / 1 IO.",
|
|
1014
|
+
"check-proof is the OFFLINE third-party AUDITOR command: it reads ONLY the proof artifact (NO journal,",
|
|
1015
|
+
" NO key, NO network) and verifies it for its kind. Hand an auditor a tree head + a proof file and",
|
|
1016
|
+
" they confirm inclusion/append-only-ness WITHOUT your log. ACCEPTED means the proof verifies against",
|
|
1017
|
+
" the head EMBEDDED in the artifact — compare that head against one you trust before relying on it.",
|
|
1018
|
+
" Exit: 0 ACCEPTED / 3 REJECTED / 2 usage / 1 IO — the SHARED 0/3 verify contract.",
|
|
1019
|
+
"The `ts` is SELF-ASSERTED (the verifier's own wall clock); the journal proves ORDERING + CONTINUITY of",
|
|
1020
|
+
" its OWN observations, and never claims \"unaltered since date T\" until a trust-root signs/timestamps it.",
|
|
1021
|
+
"",
|
|
1022
|
+
].join("\n");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* `vh journal <sub> ...` dispatcher. Mirrors the multi-level verb shape of `vh evidence`.
|
|
1027
|
+
* @param {string[]} argv the args AFTER "journal"
|
|
1028
|
+
* @param {object} io { write, writeErr }
|
|
1029
|
+
* @returns {number} exit code
|
|
1030
|
+
*/
|
|
1031
|
+
function cmdJournal(argv, io = {}) {
|
|
1032
|
+
const writeErr = io.writeErr || ((s) => process.stderr.write(s));
|
|
1033
|
+
const write = io.write || ((s) => process.stdout.write(s));
|
|
1034
|
+
const [sub, ...rest] = argv;
|
|
1035
|
+
|
|
1036
|
+
if (sub === "append") {
|
|
1037
|
+
let opts;
|
|
1038
|
+
try {
|
|
1039
|
+
opts = parseAppendArgs(rest);
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
writeErr(`error: ${e.message}\n`);
|
|
1042
|
+
return JOURNAL_EXIT.USAGE;
|
|
1043
|
+
}
|
|
1044
|
+
return runJournalAppend(opts, io);
|
|
1045
|
+
}
|
|
1046
|
+
if (sub === "verify") {
|
|
1047
|
+
let opts;
|
|
1048
|
+
try {
|
|
1049
|
+
opts = parseJournalVerifyArgs(rest);
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
writeErr(`error: ${e.message}\n`);
|
|
1052
|
+
return JOURNAL_EXIT.USAGE;
|
|
1053
|
+
}
|
|
1054
|
+
return runJournalVerify(opts, io);
|
|
1055
|
+
}
|
|
1056
|
+
// The T-63.2 transparency-log verbs: same parse-then-run shape, same shared exit contract.
|
|
1057
|
+
const logVerbs = {
|
|
1058
|
+
"tree-head": [parseTreeHeadArgs, runJournalTreeHead],
|
|
1059
|
+
"prove-inclusion": [parseProveInclusionArgs, runJournalProveInclusion],
|
|
1060
|
+
"prove-consistency": [parseProveConsistencyArgs, runJournalProveConsistency],
|
|
1061
|
+
"check-proof": [parseCheckProofArgs, runJournalCheckProof],
|
|
1062
|
+
};
|
|
1063
|
+
if (Object.prototype.hasOwnProperty.call(logVerbs, sub)) {
|
|
1064
|
+
const [parse, run] = logVerbs[sub];
|
|
1065
|
+
let opts;
|
|
1066
|
+
try {
|
|
1067
|
+
opts = parse(rest);
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
writeErr(`error: ${e.message}\n`);
|
|
1070
|
+
return JOURNAL_EXIT.USAGE;
|
|
1071
|
+
}
|
|
1072
|
+
return run(opts, io);
|
|
1073
|
+
}
|
|
1074
|
+
if (sub === undefined || sub === "-h" || sub === "--help" || sub === "help") {
|
|
1075
|
+
write(journalUsage());
|
|
1076
|
+
return sub === undefined ? JOURNAL_EXIT.USAGE : JOURNAL_EXIT.OK;
|
|
1077
|
+
}
|
|
1078
|
+
writeErr(
|
|
1079
|
+
`error: unknown journal subcommand: ${sub} (expected: append, verify, tree-head, prove-inclusion, prove-consistency, check-proof)\n`
|
|
1080
|
+
);
|
|
1081
|
+
return JOURNAL_EXIT.USAGE;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
module.exports = {
|
|
1085
|
+
JOURNAL_EXIT,
|
|
1086
|
+
buildVerifyBodyFromSeal,
|
|
1087
|
+
readJournalFile,
|
|
1088
|
+
lastEntry,
|
|
1089
|
+
firstRecordedDrift,
|
|
1090
|
+
runJournalAppend,
|
|
1091
|
+
runJournalVerify,
|
|
1092
|
+
parseAppendArgs,
|
|
1093
|
+
parseJournalVerifyArgs,
|
|
1094
|
+
journalUsage,
|
|
1095
|
+
cmdJournal,
|
|
1096
|
+
// T-63.2 — the transparency-log surface (tree-head / prove-inclusion / prove-consistency / check-proof).
|
|
1097
|
+
JOURNAL_INCLUSION_PROOF_KIND,
|
|
1098
|
+
JOURNAL_CONSISTENCY_PROOF_KIND,
|
|
1099
|
+
SELF_ASSERTED_HEAD_NOTE,
|
|
1100
|
+
CHECK_PROOF_NOTE,
|
|
1101
|
+
entryLeaves,
|
|
1102
|
+
runJournalTreeHead,
|
|
1103
|
+
runJournalProveInclusion,
|
|
1104
|
+
runJournalProveConsistency,
|
|
1105
|
+
runJournalCheckProof,
|
|
1106
|
+
parseTreeHeadArgs,
|
|
1107
|
+
parseProveInclusionArgs,
|
|
1108
|
+
parseProveConsistencyArgs,
|
|
1109
|
+
parseCheckProofArgs,
|
|
1110
|
+
};
|