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.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +883 -0
  3. package/cli/abi/ContributionRegistry.json +881 -0
  4. package/cli/agent.js +2173 -0
  5. package/cli/anchor-artifact.js +853 -0
  6. package/cli/anchor.js +400 -0
  7. package/cli/claim.js +881 -0
  8. package/cli/core/agent-commit.js +448 -0
  9. package/cli/core/agent-session.js +598 -0
  10. package/cli/core/anchor-binding.js +663 -0
  11. package/cli/core/attestation.js +580 -0
  12. package/cli/core/evidence-plans.js +495 -0
  13. package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
  14. package/cli/core/fulfill-intake.js +1082 -0
  15. package/cli/core/go-live-preflight.js +481 -0
  16. package/cli/core/license.js +534 -0
  17. package/cli/core/manifest.js +243 -0
  18. package/cli/core/packetseal.js +591 -0
  19. package/cli/core/registryArtifact.js +49 -0
  20. package/cli/core/revocation.js +539 -0
  21. package/cli/core/rfc3161.js +389 -0
  22. package/cli/core/timestamp.js +482 -0
  23. package/cli/core/trust-asof.js +479 -0
  24. package/cli/dataset.js +2950 -0
  25. package/cli/evidence.js +2227 -0
  26. package/cli/fulfill-webhook-http.js +438 -0
  27. package/cli/git.js +220 -0
  28. package/cli/hash.js +550 -0
  29. package/cli/identity.js +1072 -0
  30. package/cli/journal-cli.js +1110 -0
  31. package/cli/journal-log.js +454 -0
  32. package/cli/journal.js +334 -0
  33. package/cli/lineage.js +447 -0
  34. package/cli/list.js +287 -0
  35. package/cli/parcel.js +1509 -0
  36. package/cli/proof.js +578 -0
  37. package/cli/prove.js +300 -0
  38. package/cli/receipt.js +631 -0
  39. package/cli/registry.js +331 -0
  40. package/cli/reputation.js +344 -0
  41. package/cli/revocation.js +495 -0
  42. package/cli/serve-verify-http.js +298 -0
  43. package/cli/serve-verify.js +333 -0
  44. package/cli/show.js +339 -0
  45. package/cli/verify.js +383 -0
  46. package/cli/vh.js +3927 -0
  47. package/docs/ADOPT.md +183 -0
  48. package/docs/ADOPTION.json +11 -0
  49. package/docs/AGENTTRACE.md +247 -0
  50. package/docs/ANCHORING.md +167 -0
  51. package/docs/AUDIT.md +55 -0
  52. package/docs/CONFORMANCE.md +107 -0
  53. package/docs/DATALEDGER.md +638 -0
  54. package/docs/DECIDE.md +47 -0
  55. package/docs/DECISIONS-PENDING.md +27 -0
  56. package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
  57. package/docs/ENGINE-LEDGER.json +12 -0
  58. package/docs/EVIDENCE.md +519 -0
  59. package/docs/GO-LIVE.md +66 -0
  60. package/docs/IDENTITY.md +123 -0
  61. package/docs/INDEPENDENT-VERIFICATION.md +377 -0
  62. package/docs/INTEGRITY-JOURNAL.md +337 -0
  63. package/docs/KEY-LIFECYCLE.md +179 -0
  64. package/docs/LICENSING.md +46 -0
  65. package/docs/LINEAGE.md +307 -0
  66. package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
  67. package/docs/LOOP-HARDENING-PLAN.md +44 -0
  68. package/docs/MERKLE-LEAVES.md +113 -0
  69. package/docs/METRICS.jsonl +31 -0
  70. package/docs/MORNING.md +204 -0
  71. package/docs/PILOT.md +444 -0
  72. package/docs/PROOFPARCEL.md +227 -0
  73. package/docs/PROOFS.md +262 -0
  74. package/docs/RECEIPTS.md +341 -0
  75. package/docs/REPUTATION.md +158 -0
  76. package/docs/SDK.md +301 -0
  77. package/docs/STRATEGY-ARCHIVE.md +5055 -0
  78. package/docs/SUPERVISOR-RUNBOOK.md +52 -0
  79. package/docs/TRUST-BOUNDARIES.md +335 -0
  80. package/docs/TRUSTLEDGER.md +1976 -0
  81. package/docs/USAGE-BUDGET.json +121 -0
  82. package/docs/VERIFY-SERVICE.md +168 -0
  83. package/index.js +160 -0
  84. package/package.json +41 -0
  85. package/trustledger/build-standalone.js +796 -0
  86. package/trustledger/cli.js +3179 -0
  87. package/trustledger/close.js +391 -0
  88. package/trustledger/corpus.js +159 -0
  89. package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
  90. package/trustledger/dist/trustledger-standalone.html +6197 -0
  91. package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
  92. package/trustledger/door-core.js +442 -0
  93. package/trustledger/fixtures/bank.csv +7 -0
  94. package/trustledger/fixtures/bank.malformed.csv +3 -0
  95. package/trustledger/fixtures/bank.noalias.csv +5 -0
  96. package/trustledger/fixtures/bank.ofx +34 -0
  97. package/trustledger/fixtures/bank.real.csv +5 -0
  98. package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
  99. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
  100. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
  101. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
  102. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
  103. package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
  104. package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
  105. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
  106. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
  107. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
  108. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
  109. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
  110. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
  111. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
  112. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
  113. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
  114. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
  115. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
  116. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
  117. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
  118. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
  119. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
  120. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
  121. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
  122. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
  123. package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
  124. package/trustledger/fixtures/e2e/bank.csv +4 -0
  125. package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
  126. package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
  127. package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
  128. package/trustledger/fixtures/e2e/rentroll.csv +6 -0
  129. package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
  130. package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
  131. package/trustledger/fixtures/plans/baseline.json +25 -0
  132. package/trustledger/fixtures/plans/price-binding.example.json +27 -0
  133. package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
  134. package/trustledger/fixtures/policy/baseline.json +19 -0
  135. package/trustledger/fixtures/policy/ca-example.json +12 -0
  136. package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
  137. package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
  138. package/trustledger/fixtures/quickbooks.csv +7 -0
  139. package/trustledger/fixtures/quickbooks.real.csv +5 -0
  140. package/trustledger/fixtures/rentroll.csv +6 -0
  141. package/trustledger/fixtures/rentroll.real.csv +4 -0
  142. package/trustledger/ingest.js +1163 -0
  143. package/trustledger/lib/policy-bundled-loader.js +44 -0
  144. package/trustledger/lib/sha256-vendored.js +227 -0
  145. package/trustledger/license.js +563 -0
  146. package/trustledger/match.js +551 -0
  147. package/trustledger/plans.js +551 -0
  148. package/trustledger/policy.js +398 -0
  149. package/trustledger/public/index.html +512 -0
  150. package/trustledger/reconcile.js +1486 -0
  151. package/trustledger/report.js +887 -0
  152. package/trustledger/seal.js +854 -0
  153. package/trustledger/server.js +391 -0
  154. package/trustledger/valueproof.js +350 -0
package/cli/parcel.js ADDED
@@ -0,0 +1,1509 @@
1
+ "use strict";
2
+
3
+ // cli/parcel.js — a tamper-evident, versioned DELIVERY receipt for verifyhash (ProofParcel, T-18.2).
4
+ //
5
+ // WHY THIS EXISTS
6
+ // B2B data exchange has an expensive failure mode: a delivery dispute ("you never sent file X" / "the
7
+ // file you sent was altered"). ProofParcel issues a portable, independently-verifiable PROOF-OF-DELIVERY
8
+ // receipt that pins EXACTLY which files (names AND bytes) were delivered for a parcel — so either party
9
+ // can later re-derive the same root from the files on disk and detect any edit/rename/add/remove.
10
+ //
11
+ // This is a THIN adapter over the SHARED provenance engine (cli/core/manifest.js): a parcel manifest is
12
+ // the SAME Merkle ROOT + the SAME sorted per-file { relPath, contentHash, leaf } list a DataLedger
13
+ // dataset manifest carries — built by the IDENTICAL core builder/validator with the IDENTICAL path-bound,
14
+ // domain-separated Merkle convention from cli/hash.js (so a parcel root is the SAME value `vh hash <dir>`
15
+ // and the contract's verifyLeaf produce for the same tree). The ONLY product-specific differences are
16
+ // (a) the on-disk `kind` discriminator ("verifyhash.parcel-manifest") so the two products' manifests
17
+ // never cross-validate, (b) the human "parcel manifest" label in error messages, and (c) an OPTIONAL,
18
+ // clearly-UNTRUSTED `parcel` block.
19
+ //
20
+ // THE UNTRUSTED `parcel` BLOCK
21
+ // A caller may attach OPTIONAL delivery metadata — { parcelId?, sender?, recipient? } — describing which
22
+ // parcel this is and between which parties. Like the per-file source/license hints, these are
23
+ // SELF-ASSERTED and UNTRUSTED: they are NOT bound into the Merkle root, editing them does not change the
24
+ // root, and they prove NOTHING on their own. The manifest's `note` field says so in-band (mirroring the
25
+ // hint caveats) so a downstream reader can never mistake a self-asserted sender/recipient — or any
26
+ // delivery TIME — for a verified fact. "Delivered ON date T" needs the human-owned signing/timestamp
27
+ // trust-root (STRATEGY.md P-3), which ProofParcel inherits VERBATIM and does NOT short-circuit here.
28
+ //
29
+ // STRICTNESS
30
+ // A corrupt/edited parcel manifest must never be silently half-accepted: `readParcelManifest` validates
31
+ // strictly (the shared structural checks via the core, PLUS the parcel-block shape) and throws on the
32
+ // FIRST deviation rather than filling defaults — mirroring cli/dataset.js / cli/receipt.js / cli/proof.js.
33
+ // It deliberately does NOT re-verify the leaves against content (it has no content); re-deriving the root
34
+ // from the actual tree is the authoritative check (`vh parcel verify`).
35
+
36
+ const fs = require("fs");
37
+ const path = require("path");
38
+ const { keccak256, toUtf8Bytes } = require("ethers");
39
+ const { hashDirStream } = require("./hash");
40
+ const { diffManifest } = require("./receipt");
41
+ // The GENERIC, product-agnostic provenance engine. ProofParcel is a THIN adapter over it: the manifest
42
+ // builder/validator + the TRUST caveats live ONCE in cli/core/ and are shared with DataLedger so the
43
+ // Merkle/manifest math and — critically — the TRUST_NOTE can NEVER drift between products. The dependency
44
+ // points parcel → core (never the reverse).
45
+ const coreManifest = require("./core/manifest");
46
+ const coreTrustAsOf = require("./core/trust-asof");
47
+ // The GENERIC signed-attestation ENVELOPE engine (the wrap-don't-edit invariant, the supported `scheme`
48
+ // list, signer recovery, the OFFLINE verifier). ProofParcel's attest/verify-attest are THIN adapters over
49
+ // it — the SAME core `vh dataset attest`/`verify-attest` use — so the envelope machinery can never drift
50
+ // between products. parcel → core only (no back-edge).
51
+ const coreAttestation = require("./core/attestation");
52
+ const coreTimestamp = require("./core/timestamp");
53
+
54
+ // On-disk schema discriminator. A parcel manifest carries its OWN kind + version, DISTINCT from the
55
+ // dataset manifest kind (cli/dataset.js), the receipt kinds (cli/receipt.js), and the proof-artifact kind
56
+ // (cli/proof.js) — so a dataset manifest, a random JSON file, a receipt, or a proof artifact is never
57
+ // misread as a parcel manifest, and (critically) the two product kinds NEVER cross-validate.
58
+ const PARCEL_MANIFEST_KIND = "verifyhash.parcel-manifest";
59
+ const PARCEL_MANIFEST_SCHEMA_VERSION = 1;
60
+ const SUPPORTED_PARCEL_MANIFEST_SCHEMA_VERSIONS = Object.freeze([1]);
61
+
62
+ // Same hex shape the whole product family validates against (sourced from the shared core so it can never
63
+ // drift).
64
+ const HEX32_RE = coreManifest.HEX32_RE;
65
+
66
+ // The SHARED trust caveat (verbatim from core — ONE copy across the family, can never drift).
67
+ const TRUST_NOTE = coreManifest.TRUST_NOTE;
68
+
69
+ // ProofParcel-specific caveat, appended to the shared TRUST_NOTE wherever a human-output run LEADS with
70
+ // trust posture. It states the parcel-specific boundary: the receipt binds the delivered file SET to a
71
+ // root (tamper-evidence), but the `parcel` block (parcelId/sender/recipient) and any delivery TIME are
72
+ // UNTRUSTED self-asserted metadata and are NOT a trusted timestamp — "delivered ON date T" needs the
73
+ // human-owned signing/timestamp trust-root P-3 (cross-link), which this offline receipt does NOT provide.
74
+ const PARCEL_TRUST_NOTE =
75
+ "PARCEL CAVEAT: this receipt binds the delivered file SET to a Merkle root and proves tamper-evidence " +
76
+ "(any edit/rename/add/remove changes the root). The `parcel` block (parcelId/sender/recipient) and any " +
77
+ "delivery TIME are UNTRUSTED, self-asserted metadata NOT bound into the root — this is NOT a trusted " +
78
+ "timestamp. 'Delivered ON date T' needs the human-owned signing/timestamp trust-root (STRATEGY.md P-3).";
79
+
80
+ // The recognized fields of the OPTIONAL `parcel` block. Each is an OPTIONAL self-asserted string; an
81
+ // unknown field is rejected so junk never lands in the manifest masquerading as structured metadata.
82
+ const PARCEL_BLOCK_FIELDS = Object.freeze(["parcelId", "sender", "recipient"]);
83
+
84
+ // ProofParcel's manifest framing, passed to the GENERIC core builder/validator. The core does the shared
85
+ // Merkle/manifest math + structural validation; this object supplies ONLY the ProofParcel-specific framing.
86
+ const MANIFEST_CFG = Object.freeze({
87
+ kind: PARCEL_MANIFEST_KIND,
88
+ schemaVersion: PARCEL_MANIFEST_SCHEMA_VERSION,
89
+ supportedSchemaVersions: SUPPORTED_PARCEL_MANIFEST_SCHEMA_VERSIONS,
90
+ note: TRUST_NOTE,
91
+ label: "parcel manifest",
92
+ });
93
+
94
+ /**
95
+ * Normalize a raw parcel-metadata block into a plain { parcelId?, sender?, recipient? } object of strings.
96
+ * Rejects a non-object, an unknown field, or a non-string value, so junk never lands in the manifest.
97
+ * Returns undefined when the block is absent OR carries no labeled field (so an empty `parcel` block never
98
+ * litters the manifest with `{}`). PURE — no side effects.
99
+ *
100
+ * @param {object|null|undefined} raw
101
+ * @returns {{parcelId?:string,sender?:string,recipient?:string}|undefined}
102
+ */
103
+ function normalizeParcelBlock(raw) {
104
+ if (raw == null) return undefined;
105
+ if (typeof raw !== "object" || Array.isArray(raw)) {
106
+ throw new Error("parcel metadata must be an object with parcelId/sender/recipient");
107
+ }
108
+ for (const k of Object.keys(raw)) {
109
+ if (!PARCEL_BLOCK_FIELDS.includes(k)) {
110
+ throw new Error(
111
+ `unknown parcel metadata field: ${JSON.stringify(k)} ` +
112
+ `(allowed: ${PARCEL_BLOCK_FIELDS.join(", ")})`
113
+ );
114
+ }
115
+ }
116
+ const out = {};
117
+ for (const k of PARCEL_BLOCK_FIELDS) {
118
+ if (raw[k] === undefined || raw[k] === null) continue;
119
+ if (typeof raw[k] !== "string") {
120
+ throw new Error(`parcel metadata ${k} must be a string`);
121
+ }
122
+ out[k] = raw[k];
123
+ }
124
+ return Object.keys(out).length > 0 ? out : undefined;
125
+ }
126
+
127
+ /**
128
+ * Validate the OPTIONAL `parcel` block on a parsed manifest (when present). Throws on a non-object, an
129
+ * unknown field, or a non-string value. Shared by both the build path (defensive re-check) and the read
130
+ * path. Does NOT mutate.
131
+ * @param {any} block
132
+ */
133
+ function validateParcelBlock(block) {
134
+ if (block === undefined || block === null) return;
135
+ if (typeof block !== "object" || Array.isArray(block)) {
136
+ throw new Error("parcel manifest `parcel` block must be an object when present");
137
+ }
138
+ for (const k of Object.keys(block)) {
139
+ if (!PARCEL_BLOCK_FIELDS.includes(k)) {
140
+ throw new Error(
141
+ `parcel manifest \`parcel\` block has an unknown field: ${JSON.stringify(k)} ` +
142
+ `(allowed: ${PARCEL_BLOCK_FIELDS.join(", ")})`
143
+ );
144
+ }
145
+ if (typeof block[k] !== "string") {
146
+ throw new Error(`parcel manifest \`parcel\`.${k} must be a string when present`);
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Build a normalized, fully-validated PARCEL-manifest object from a streamed directory result plus optional
153
+ * per-file hints and an optional UNTRUSTED parcel block. THIN wrapper over the generic core builder: the
154
+ * core does the shared hint-normalization, the Merkle/manifest assembly, and the strict structural
155
+ * validation (kind/schema/hex root/per-file leaf == pathLeaf(relPath,contentHash)); ProofParcel supplies
156
+ * ONLY its framing (MANIFEST_CFG) and attaches the untrusted `parcel` block AFTER the root is fixed (so the
157
+ * block can never be bound into the root). Throws if the result is malformed, so a corrupt manifest is
158
+ * never produced.
159
+ *
160
+ * @param {{ root: string, leaves: {path:string,contentHash:string,leaf:string}[] }} built
161
+ * the object cli/hash.js › hashDirStream returns
162
+ * @param {object} [opts]
163
+ * @param {Object<string,{source?:string,license?:string}>} [opts.hints] OPTIONAL untrusted per-file hints
164
+ * @param {{parcelId?:string,sender?:string,recipient?:string}} [opts.parcel] OPTIONAL untrusted delivery metadata
165
+ * @returns {object} a validated parcel-manifest object
166
+ */
167
+ function buildParcelManifest(built, opts = {}) {
168
+ // 1. Core builds the shared manifest (root + per-file leaves + optional hints) — identical math to a
169
+ // dataset manifest. The root is computed from (relPath, content) pairs ONLY.
170
+ const manifest = coreManifest.buildItemManifest(built, MANIFEST_CFG, { hints: opts.hints });
171
+
172
+ // 2. Attach the OPTIONAL, UNTRUSTED parcel block AFTER the root is fixed. Recorded under an explicitly
173
+ // self-asserted `parcel` key; it does NOT participate in any leaf or the root.
174
+ const parcel = normalizeParcelBlock(opts.parcel);
175
+ if (parcel) manifest.parcel = parcel;
176
+
177
+ // 3. Re-validate via the full parcel validator so the build path and the read path share ONE strict
178
+ // definition of a valid parcel manifest (core structural checks PLUS the parcel-block shape).
179
+ return validateParcelManifest(manifest);
180
+ }
181
+
182
+ /**
183
+ * Strictly validate a parsed parcel-manifest object. Runs the GENERIC core validator with ProofParcel's
184
+ * framing (MANIFEST_CFG) — which enforces the shared structural rules and, via the distinct `kind`, REJECTS
185
+ * a dataset manifest (and vice-versa, the dataset validator rejects this) so the two product kinds never
186
+ * cross-validate — then ADDITIONALLY validates the optional `parcel` block. Throws on the FIRST problem;
187
+ * never mutates and never fills defaults.
188
+ * @param {any} obj
189
+ * @returns {object} the same object, if valid
190
+ */
191
+ function validateParcelManifest(obj) {
192
+ coreManifest.validateItemManifest(obj, MANIFEST_CFG);
193
+ validateParcelBlock(obj.parcel);
194
+ return obj;
195
+ }
196
+
197
+ /**
198
+ * Read, parse, and STRICTLY validate the parcel manifest at `manifestPath`. Throws on a missing file,
199
+ * invalid JSON, or ANY schema deviation (so a malformed/edited/foreign manifest — including a DATASET
200
+ * manifest — is rejected, never half-accepted).
201
+ * @param {string} manifestPath
202
+ * @returns {object} the validated parcel-manifest object
203
+ */
204
+ function readParcelManifest(manifestPath) {
205
+ if (!manifestPath || typeof manifestPath !== "string") {
206
+ throw new Error("readParcelManifest requires a manifest file path");
207
+ }
208
+ let raw;
209
+ try {
210
+ raw = fs.readFileSync(manifestPath, "utf8");
211
+ } catch (e) {
212
+ throw new Error(`cannot read parcel manifest at ${manifestPath}: ${e.message}`);
213
+ }
214
+ let obj;
215
+ try {
216
+ obj = JSON.parse(raw);
217
+ } catch (e) {
218
+ throw new Error(`parcel manifest at ${manifestPath} is not valid JSON: ${e.message}`);
219
+ }
220
+ return validateParcelManifest(obj);
221
+ }
222
+
223
+ /**
224
+ * Validate and write a parcel manifest to `outPath` as pretty JSON + a trailing newline. The ONLY side
225
+ * effect is the file write at the caller-chosen path (never silently the cwd), and it throws (before
226
+ * writing) if the object is not a valid parcel manifest, so a corrupt manifest never lands on disk.
227
+ * @param {object} obj a parcel manifest (typically from buildParcelManifest)
228
+ * @param {string} outPath destination file path (caller-chosen)
229
+ * @returns {object} the validated object that was written
230
+ */
231
+ function writeParcelManifest(obj, outPath) {
232
+ if (!outPath || typeof outPath !== "string") {
233
+ throw new Error("writeParcelManifest requires an --out path");
234
+ }
235
+ validateParcelManifest(obj);
236
+ fs.writeFileSync(outPath, JSON.stringify(obj, null, 2) + "\n");
237
+ return obj;
238
+ }
239
+
240
+ /**
241
+ * Orchestrate `vh parcel build <dir> --out <p>`: stream-hash the delivered tree, build the parcel manifest
242
+ * (with optional untrusted hints + parcel metadata), write it to the caller's --out path, and return a
243
+ * small summary. Writes ONLY to `outPath` — no cwd litter. Every human-output run LEADS with the shared
244
+ * TRUST_NOTE (verbatim) + the parcel-specific caveat. OFFLINE: no provider, no key, no network.
245
+ *
246
+ * @param {object} opts
247
+ * @param {string} opts.dir delivered directory to manifest (walked recursively)
248
+ * @param {string} opts.out where to write the manifest (REQUIRED — never defaulted to cwd)
249
+ * @param {Object<string,{source?:string,license?:string}>} [opts.hints] optional untrusted per-file hints
250
+ * @param {{parcelId?:string,sender?:string,recipient?:string}} [opts.parcel] optional untrusted delivery metadata
251
+ * @param {boolean} [opts.json] emit machine-readable JSON instead of the human summary
252
+ * @param {(s:string)=>void} [opts.stdout] sink for stdout (default process.stdout.write); injectable for tests
253
+ * @returns {{ root: string, fileCount: number, out: string, parcel: object|undefined }}
254
+ */
255
+ function runParcelBuild(opts) {
256
+ if (!opts || typeof opts !== "object") throw new Error("runParcelBuild requires options");
257
+ const { dir, out, hints, parcel } = opts;
258
+ const write = opts.stdout || ((s) => process.stdout.write(s));
259
+ if (!dir) throw new Error("runParcelBuild requires a parcel <dir>");
260
+ if (!out) throw new Error("runParcelBuild requires an --out <manifest> path");
261
+
262
+ // Resolve to an absolute path so the manifest is written EXACTLY where the caller asked, regardless of
263
+ // cwd. statSync errors clearly (ENOENT / not a dir) before we walk anything.
264
+ const dirAbs = path.resolve(dir);
265
+ const stat = fs.statSync(dirAbs);
266
+ if (!stat.isDirectory()) {
267
+ throw new Error(`parcel target is not a directory: ${dir}`);
268
+ }
269
+ const outAbs = path.resolve(out);
270
+
271
+ const built = hashDirStream(dirAbs); // streams each file; never loads all content at once
272
+ const manifest = buildParcelManifest(built, { hints, parcel });
273
+ writeParcelManifest(manifest, outAbs);
274
+
275
+ if (opts.json) {
276
+ write(
277
+ JSON.stringify({
278
+ root: manifest.root,
279
+ fileCount: manifest.fileCount,
280
+ out: outAbs,
281
+ parcel: manifest.parcel,
282
+ }) + "\n"
283
+ );
284
+ } else {
285
+ write(` TRUST: ${TRUST_NOTE}\n`);
286
+ write(` ${PARCEL_TRUST_NOTE}\n`);
287
+ write(`parcel manifest written: ${outAbs}\n`);
288
+ write(` root: ${manifest.root}\n`);
289
+ write(` files: ${manifest.fileCount}\n`);
290
+ if (manifest.parcel) {
291
+ write(
292
+ " parcel (UNTRUSTED self-asserted metadata; NOT bound into the root): " +
293
+ JSON.stringify(manifest.parcel) +
294
+ "\n"
295
+ );
296
+ } else {
297
+ write(" parcel: (none provided)\n");
298
+ }
299
+ }
300
+ return {
301
+ root: manifest.root,
302
+ fileCount: manifest.fileCount,
303
+ out: outAbs,
304
+ parcel: manifest.parcel,
305
+ };
306
+ }
307
+
308
+ // Possible outcomes of a `vh parcel verify` run. The AUTHORITATIVE verdict is recomputed-root vs
309
+ // manifest-root — never the per-file diff (which only LOCALIZES which file moved). Mirrors
310
+ // cli/dataset.js › VERIFY_STATUS so all verify gates share ONE vocabulary AND ONE exit contract.
311
+ const VERIFY_STATUS = Object.freeze({
312
+ MATCH: "MATCH", // root re-derived from the FRESH tree equals the manifest's recorded root
313
+ MISMATCH: "MISMATCH", // it does NOT — a file was added/removed/changed/renamed since the manifest
314
+ });
315
+
316
+ /**
317
+ * Re-derive the parcel root from a FRESH copy of the delivered files at `dir` and compare it to the
318
+ * (UNTRUSTED) manifest's recorded root, then localize any divergence to specific files.
319
+ *
320
+ * TRUST POSTURE. The manifest is an UNTRUSTED hint: the AUTHORITATIVE MATCH/MISMATCH is
321
+ * `recomputed-root === manifest-root`, recomputed here from the actual bytes on disk via the SAME
322
+ * path-bound Merkle convention `vh hash <dir>` and the on-chain verifyLeaf use. The per-file
323
+ * ADDED/REMOVED/CHANGED diff is a CONVENIENCE that says WHICH file diverged; it never decides the verdict
324
+ * (so even a manifest with a hand-edited `root` cannot fake a MATCH — the root is recomputed, not read
325
+ * from the manifest). The untrusted `parcel` block plays NO part in the verdict. Fully OFFLINE: no
326
+ * provider, no key, no network.
327
+ *
328
+ * The diff REUSES the SAME receipt-manifest diff core (`cli/receipt.js › diffManifest`, the function
329
+ * `cli/verify.js` and `vh dataset verify` use): a `CHANGED` entry carries old→new contentHash; a rename
330
+ * surfaces as one REMOVED (old path) + one ADDED (new path), because the path is bound into the leaf.
331
+ *
332
+ * @param {object} opts
333
+ * @param {string} opts.dir delivered directory to re-derive the root from (the FRESH copy)
334
+ * @param {string} opts.manifest path to a manifest written by `vh parcel build` (UNTRUSTED hint)
335
+ * @param {boolean}[opts.json] emit a machine-readable JSON object instead of the human block
336
+ * @param {(s:string)=>void}[opts.stdout] sink for stdout (default process.stdout.write); injectable for tests
337
+ * @returns {{
338
+ * status: "MATCH"|"MISMATCH",
339
+ * recomputedRoot: string,
340
+ * manifestRoot: string,
341
+ * fileCount: number,
342
+ * parcel: object|undefined,
343
+ * diff: { added: any[], removed: any[], changed: any[], unchanged: any[], identical: boolean }
344
+ * }}
345
+ */
346
+ function runParcelVerify(opts) {
347
+ if (!opts || typeof opts !== "object") throw new Error("runParcelVerify requires options");
348
+ const { dir, manifest: manifestPath } = opts;
349
+ const write = opts.stdout || ((s) => process.stdout.write(s));
350
+ if (!dir) throw new Error("runParcelVerify requires a parcel <dir>");
351
+ if (!manifestPath) throw new Error("runParcelVerify requires a --manifest <p> path");
352
+
353
+ // Resolve so we read EXACTLY where the caller asked regardless of cwd. statSync errors clearly
354
+ // (ENOENT / not a dir) before we walk anything — and BEFORE we trust the manifest at all.
355
+ const dirAbs = path.resolve(dir);
356
+ const stat = fs.statSync(dirAbs);
357
+ if (!stat.isDirectory()) {
358
+ throw new Error(`parcel target is not a directory: ${dir}`);
359
+ }
360
+
361
+ // The manifest is an untrusted hint, but it must be STRUCTURALLY sound or we cannot diff against it
362
+ // (readParcelManifest rejects a corrupt/edited/foreign manifest — including a DATASET manifest — rather
363
+ // than half-accepting it).
364
+ const manifest = readParcelManifest(manifestPath);
365
+
366
+ // Re-derive the root + per-file leaves from the FRESH tree (streamed; never loads all content).
367
+ const built = hashDirStream(dirAbs);
368
+ const recomputedRoot = built.root;
369
+ const manifestRoot = manifest.root;
370
+
371
+ // AUTHORITATIVE verdict: recomputed root vs manifest root. Case-insensitive hex compare.
372
+ const status =
373
+ recomputedRoot.toLowerCase() === manifestRoot.toLowerCase()
374
+ ? VERIFY_STATUS.MATCH
375
+ : VERIFY_STATUS.MISMATCH;
376
+
377
+ // Localize WHICH file diverged using the SAME diff core cli/verify.js / vh dataset verify use. The
378
+ // manifest entries are keyed by `relPath`; diffManifest expects `path`, so map across.
379
+ const recordedManifest = manifest.files.map((f) => ({
380
+ path: f.relPath,
381
+ contentHash: f.contentHash,
382
+ leaf: f.leaf,
383
+ }));
384
+ const diff = diffManifest(recordedManifest, built.leaves);
385
+
386
+ if (opts.json) {
387
+ write(
388
+ JSON.stringify({
389
+ status,
390
+ recomputedRoot,
391
+ manifestRoot,
392
+ fileCount: built.leaves.length,
393
+ parcel: manifest.parcel,
394
+ diff,
395
+ }) + "\n"
396
+ );
397
+ } else {
398
+ for (const line of formatParcelVerify({
399
+ status,
400
+ recomputedRoot,
401
+ manifestRoot,
402
+ parcel: manifest.parcel,
403
+ diff,
404
+ })) {
405
+ write(line + "\n");
406
+ }
407
+ }
408
+ return {
409
+ status,
410
+ recomputedRoot,
411
+ manifestRoot,
412
+ fileCount: built.leaves.length,
413
+ parcel: manifest.parcel,
414
+ diff,
415
+ };
416
+ }
417
+
418
+ /**
419
+ * Render a parcel-verify result as the human-readable block the CLI prints. LEADS with the shared
420
+ * TRUST_NOTE (verbatim) + the parcel-specific caveat, then the authoritative root comparison, then the
421
+ * per-file diff (labeled as localization, never the verdict), then the untrusted parcel block.
422
+ * @param {{status:string,recomputedRoot:string,manifestRoot:string,parcel:object|undefined,diff:object}} r
423
+ * @returns {string[]} lines
424
+ */
425
+ function formatParcelVerify(r) {
426
+ const lines = [
427
+ ` TRUST: ${TRUST_NOTE}`,
428
+ ` ${PARCEL_TRUST_NOTE}`,
429
+ "",
430
+ ` parcel verify: ${r.status}`,
431
+ ` recomputed root: ${r.recomputedRoot} (re-derived from the files on disk — AUTHORITATIVE)`,
432
+ ` manifest root: ${r.manifestRoot} (untrusted hint)`,
433
+ ];
434
+ if (r.status === VERIFY_STATUS.MATCH) {
435
+ lines.push(
436
+ " The delivered files are byte-for-byte (and name-for-name) what the parcel manifest committed to."
437
+ );
438
+ } else {
439
+ lines.push(
440
+ " The delivered files do NOT match the manifest: a file was added, removed, changed, or renamed",
441
+ " since the manifest was built (the root commits to file NAMES and bytes)."
442
+ );
443
+ }
444
+ const d = r.diff;
445
+ lines.push("", " --- per-file diff (localization; the root comparison above is the verdict) ---");
446
+ if (d.identical) {
447
+ lines.push(" files: IDENTICAL — every file matches the manifest (no ADDED/REMOVED/CHANGED).");
448
+ } else {
449
+ lines.push(
450
+ ` files: ${d.changed.length} CHANGED, ${d.added.length} ADDED, ${d.removed.length} REMOVED` +
451
+ ` (${d.unchanged.length} unchanged)`
452
+ );
453
+ for (const c of d.changed) {
454
+ lines.push(` CHANGED ${c.path}`);
455
+ lines.push(` old: ${c.oldContentHash}`);
456
+ lines.push(` new: ${c.newContentHash}`);
457
+ }
458
+ for (const a of d.added) {
459
+ lines.push(` ADDED ${a.path} (${a.contentHash}) present now, not in the manifest`);
460
+ }
461
+ for (const rm of d.removed) {
462
+ lines.push(` REMOVED ${rm.path} (${rm.contentHash}) in the manifest, gone now`);
463
+ }
464
+ }
465
+ // The untrusted parcel block, ALWAYS flagged as self-asserted and never part of the verdict.
466
+ lines.push("");
467
+ if (r.parcel) {
468
+ lines.push(
469
+ " parcel (UNTRUSTED self-asserted metadata; NOT bound into the root, plays NO part in the verdict): " +
470
+ JSON.stringify(r.parcel)
471
+ );
472
+ } else {
473
+ lines.push(" parcel: (none recorded in the manifest)");
474
+ }
475
+ return lines;
476
+ }
477
+
478
+ // =================================================================================================
479
+ // `vh parcel attest <manifest> [--json] [--out <p>]` — the deterministic, canonical UNSIGNED attestation
480
+ // payload a human signing/timestamp trust-root (P-3) will sign over a DELIVERED parcel.
481
+ //
482
+ // WHY THIS EXISTS
483
+ // A ProofParcel manifest binds the delivered file SET to a Merkle root. But "you accepted delivery of
484
+ // THIS exact parcel on date T" needs a SIGNATURE over a canonical IDENTITY — and standing up a real
485
+ // signing key / timestamp anchor is a HUMAN-owned trust-root (P-3, needs-human). The deterministic,
486
+ // canonical BYTES that human/service would sign are fully buildable NOW, purely offline. Producing them
487
+ // turns the future human signing step into "sign THIS exact file" — a one-liner.
488
+ //
489
+ // `vh parcel attest <manifest>` reads the manifest via the SAME strict `readParcelManifest` (a corrupt/
490
+ // foreign manifest — INCLUDING a dataset manifest — is rejected, never half-accepted) and emits a
491
+ // versioned, strictly-validated attestation ENVELOPE that commits to the parcel IDENTITY a signer signs
492
+ // over: the Merkle `root`, the `fileCount`, and a canonical `manifestDigest` over the COMMITTED file set.
493
+ // PURELY OFFLINE: no tree, no provider, no key, no network.
494
+ //
495
+ // It uses the SAME canonicalization as `vh dataset attest` (keccak256 over the committed
496
+ // {relPath,contentHash,leaf} entries, ordered by relPath, no insignificant whitespace) — the UNTRUSTED
497
+ // `parcel` block (parcelId/sender/recipient) and the per-file hints are DELIBERATELY EXCLUDED: they are
498
+ // not bound into the root, so a signer must not commit to them. So a parcel and a dataset built over the
499
+ // SAME files yield the SAME manifestDigest; the products are distinguished by the SIGNED-CONTAINER kind
500
+ // (below), not the unsigned identity.
501
+ //
502
+ // UNSIGNED MARKER (never imply a signature/timestamp exists)
503
+ // The envelope carries `signed: false` / `signature: null` and an in-band `note` pointing at the
504
+ // human-owned trust-root (P-3). Until a signature is attached it proves only the same set-membership /
505
+ // identity the manifest already does — NOT "delivered/unaltered since a date T".
506
+
507
+ const PARCEL_ATTESTATION_KIND = "verifyhash.parcel-attestation";
508
+ const PARCEL_ATTESTATION_SCHEMA_VERSION = 1;
509
+ const SUPPORTED_PARCEL_ATTESTATION_SCHEMA_VERSIONS = Object.freeze([1]);
510
+
511
+ // The standing trust caveat carried IN-BAND in every UNSIGNED parcel-attestation envelope. Load-bearing:
512
+ // a reader (or the future human signer) must never mistake this UNSIGNED payload for a time-anchored
513
+ // delivery proof. It states plainly that signing is the human-owned trust-root (P-3, needs-human).
514
+ const PARCEL_ATTESTATION_TRUST_NOTE =
515
+ "This is the UNSIGNED parcel attestation payload. It commits to the parcel IDENTITY (Merkle root, " +
516
+ "fileCount, and a canonical manifestDigest over the delivered file set). It is NOT signed and NOT " +
517
+ "timestamped: `signed` is false and `signature` is null until a human/timestamp trust-root fills them " +
518
+ "in. Standing up a real signing key / timestamp anchor is the human-owned trust-root (needs-human, " +
519
+ "P-3). Until a signature is attached, this proves only the same set-membership / identity the manifest " +
520
+ "already does — NOT that the parcel was DELIVERED, or is unaltered, since a date T.";
521
+
522
+ /**
523
+ * Canonically serialize the parcel manifest's COMMITTED file set to the exact UTF-8 bytes the
524
+ * `manifestDigest` is taken over. IDENTICAL canonicalization to `vh dataset attest`: only the
525
+ * root-committed fields { relPath, contentHash, leaf } are included (the untrusted per-file hints AND the
526
+ * untrusted `parcel` block are excluded), each entry's keys emitted in the FIXED order
527
+ * [relPath, contentHash, leaf], entries ordered by relPath ascending, the array JSON-serialized with NO
528
+ * insignificant whitespace. Pure (no mutation).
529
+ * @param {object} manifest a validated parcel-manifest object (from readParcelManifest/validateParcelManifest)
530
+ * @returns {string} the canonical JSON string of the committed file set
531
+ */
532
+ function canonicalParcelFiles(manifest) {
533
+ const entries = manifest.files.map((f) => ({
534
+ relPath: f.relPath,
535
+ contentHash: f.contentHash,
536
+ leaf: f.leaf,
537
+ }));
538
+ entries.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
539
+ return JSON.stringify(entries);
540
+ }
541
+
542
+ /**
543
+ * Compute the canonical `manifestDigest`: keccak256 over the canonical serialization of the parcel
544
+ * manifest's committed file set (see canonicalParcelFiles). Deterministic; any edit/rename/add/remove to
545
+ * the committed set changes it. Pure.
546
+ * @param {object} manifest a validated parcel-manifest object
547
+ * @returns {string} a 0x-prefixed 32-byte hex digest
548
+ */
549
+ function parcelManifestDigest(manifest) {
550
+ return keccak256(toUtf8Bytes(canonicalParcelFiles(manifest)));
551
+ }
552
+
553
+ /**
554
+ * Build a normalized, fully-validated UNSIGNED parcel-attestation envelope from a validated parcel-manifest
555
+ * object. Commits to the parcel identity (root, fileCount, manifestDigest) plus the standing trust caveat,
556
+ * with explicit `signed:false`/`signature:null` markers. PURE: no I/O, no key, no network. The UNTRUSTED
557
+ * `parcel` block is DELIBERATELY excluded — a signer commits to the file SET identity, never the
558
+ * self-asserted metadata. Throws (via validateParcelAttestation) if the result is malformed.
559
+ * @param {object} manifest a validated parcel-manifest object (from readParcelManifest)
560
+ * @returns {object} a validated parcel-attestation envelope
561
+ */
562
+ function buildParcelAttestation(manifest) {
563
+ validateParcelManifest(manifest);
564
+ const env = {
565
+ kind: PARCEL_ATTESTATION_KIND,
566
+ schemaVersion: PARCEL_ATTESTATION_SCHEMA_VERSION,
567
+ note: PARCEL_ATTESTATION_TRUST_NOTE,
568
+ root: manifest.root,
569
+ fileCount: manifest.files.length,
570
+ manifestDigest: parcelManifestDigest(manifest),
571
+ signed: false,
572
+ signature: null,
573
+ };
574
+ validateParcelAttestation(env);
575
+ return env;
576
+ }
577
+
578
+ /**
579
+ * Strictly validate a parsed UNSIGNED parcel-attestation envelope. Throws an Error describing the FIRST
580
+ * problem; never mutates and never fills defaults. REJECTS a wrong kind/schemaVersion (so a DATASET
581
+ * attestation never cross-validates), a missing/!hex root or manifestDigest, a bad fileCount, or any
582
+ * envelope that claims to be signed (this UNSIGNED payload must never imply a signature).
583
+ * @param {any} obj
584
+ * @returns {object} the same object, if valid
585
+ */
586
+ function validateParcelAttestation(obj) {
587
+ if (obj == null || typeof obj !== "object" || Array.isArray(obj)) {
588
+ throw new Error("parcel attestation must be a JSON object");
589
+ }
590
+ if (obj.kind !== PARCEL_ATTESTATION_KIND) {
591
+ throw new Error(
592
+ `not a verifyhash parcel attestation (kind: ${JSON.stringify(obj.kind)}; expected ` +
593
+ `${JSON.stringify(PARCEL_ATTESTATION_KIND)})`
594
+ );
595
+ }
596
+ if (!SUPPORTED_PARCEL_ATTESTATION_SCHEMA_VERSIONS.includes(obj.schemaVersion)) {
597
+ throw new Error(
598
+ `unsupported parcel attestation schemaVersion: ${JSON.stringify(obj.schemaVersion)} ` +
599
+ `(this build understands ${JSON.stringify(SUPPORTED_PARCEL_ATTESTATION_SCHEMA_VERSIONS)})`
600
+ );
601
+ }
602
+ for (const f of ["root", "manifestDigest"]) {
603
+ if (typeof obj[f] !== "string" || !HEX32_RE.test(obj[f])) {
604
+ throw new Error(
605
+ `parcel attestation ${f} must be a 0x-prefixed 32-byte hex string, got: ${String(obj[f])}`
606
+ );
607
+ }
608
+ }
609
+ if (!Number.isInteger(obj.fileCount) || obj.fileCount < 1) {
610
+ throw new Error(
611
+ `parcel attestation fileCount must be a positive integer, got: ${String(obj.fileCount)}`
612
+ );
613
+ }
614
+ // The UNSIGNED payload must NEVER imply a signature/timestamp. `signed` exactly false, `signature`
615
+ // exactly null — attaching a real signature is the human-owned trust-root (P-3).
616
+ if (obj.signed !== false) {
617
+ throw new Error(
618
+ `parcel attestation signed must be false (this build emits/reads only the UNSIGNED payload; ` +
619
+ `attaching a real signature is the human-owned trust-root, P-3), got: ${String(obj.signed)}`
620
+ );
621
+ }
622
+ if (obj.signature !== null) {
623
+ throw new Error(
624
+ `parcel attestation signature must be null in the UNSIGNED payload, got: ${String(obj.signature)}`
625
+ );
626
+ }
627
+ return obj;
628
+ }
629
+
630
+ /**
631
+ * Serialize an UNSIGNED parcel-attestation envelope to its canonical, byte-deterministic bytes: a FIXED
632
+ * top-level key order, NO insignificant whitespace, a single trailing newline. Two runs over the same
633
+ * manifest produce an identical string — the property that makes signing the bytes well-defined.
634
+ * @param {object} env a validated parcel-attestation envelope
635
+ * @returns {string} the canonical serialization (newline-terminated)
636
+ */
637
+ function serializeParcelAttestation(env) {
638
+ validateParcelAttestation(env);
639
+ const canonical = {
640
+ kind: env.kind,
641
+ schemaVersion: env.schemaVersion,
642
+ note: env.note,
643
+ root: env.root,
644
+ fileCount: env.fileCount,
645
+ manifestDigest: env.manifestDigest,
646
+ signed: env.signed,
647
+ signature: env.signature,
648
+ };
649
+ return JSON.stringify(canonical) + "\n";
650
+ }
651
+
652
+ /**
653
+ * Read, parse, and STRICTLY validate the UNSIGNED parcel-attestation envelope at `attestationPath`. The
654
+ * strict reader round-trips with serializeParcelAttestation; a malformed/edited/foreign (e.g. a DATASET)
655
+ * envelope is rejected, never half-accepted. Throws on a missing file or invalid JSON too.
656
+ * @param {string} attestationPath
657
+ * @returns {object} the validated envelope
658
+ */
659
+ function readParcelAttestation(attestationPath) {
660
+ if (!attestationPath || typeof attestationPath !== "string") {
661
+ throw new Error("readParcelAttestation requires an attestation file path");
662
+ }
663
+ let raw;
664
+ try {
665
+ raw = fs.readFileSync(attestationPath, "utf8");
666
+ } catch (e) {
667
+ throw new Error(`cannot read parcel attestation at ${attestationPath}: ${e.message}`);
668
+ }
669
+ let obj;
670
+ try {
671
+ obj = JSON.parse(raw);
672
+ } catch (e) {
673
+ throw new Error(`parcel attestation at ${attestationPath} is not valid JSON: ${e.message}`);
674
+ }
675
+ return validateParcelAttestation(obj);
676
+ }
677
+
678
+ // =================================================================================================
679
+ // SIGNED parcel-attestation container — a detached signature WRAPPED AROUND the canonical UNSIGNED parcel
680
+ // attestation, over the SAME generic core `vh dataset` uses. ProofParcel's OWN container `kind` means a
681
+ // DATASET signed-container does NOT cross-verify as a parcel one (and vice-versa) even though the UNSIGNED
682
+ // identity bytes can coincide for the same files.
683
+
684
+ const SIGNED_PARCEL_ATTESTATION_KIND = "verifyhash.parcel-attestation-signed";
685
+ const SIGNED_PARCEL_ATTESTATION_SCHEMA_VERSION = 1;
686
+ const SUPPORTED_SIGNED_PARCEL_ATTESTATION_SCHEMA_VERSIONS = Object.freeze([1]);
687
+
688
+ // The detached signature schemes, sourced from cli/core so the supported set is the IDENTICAL one shared
689
+ // across the product family. `eip191-personal-sign` = EIP-191 personal_sign over the canonical UNSIGNED
690
+ // parcel-attestation bytes (a 65-byte r||s||v secp256k1 signature).
691
+ const SIGNED_PARCEL_ATTESTATION_SCHEMES = coreAttestation.SIGNED_ATTESTATION_SCHEMES;
692
+
693
+ // The standing trust caveat carried IN-BAND in every SIGNED parcel container. REUSES the shared TRUST_NOTE
694
+ // VERBATIM (so the family caveats never drift), adds the parcel-specific caveat (PARCEL_TRUST_NOTE,
695
+ // verbatim), and the signed-container-specific assertion: it asserts the holder of `signer`'s key vouched
696
+ // for THIS parcel identity at signing time; it does NOT prove a delivery TIMESTAMP (still P-3).
697
+ const SIGNED_PARCEL_ATTESTATION_TRUST_NOTE =
698
+ "This is a SIGNED parcel attestation container: it wraps (never edits) the EXACT canonical UNSIGNED " +
699
+ "parcel-attestation bytes in `attestation` and attaches a detached signature. It asserts that the " +
700
+ "holder of the `signer` key vouched for THIS parcel identity (the embedded root, fileCount, " +
701
+ "manifestDigest) at signing time. It does NOT by itself prove a trustworthy delivery TIMESTAMP: " +
702
+ '"delivered/unaltered since a date T" still needs the human-owned signing/timestamp trust-root ' +
703
+ "(needs-human, P-3). Every caveat of the embedded UNSIGNED payload still applies. " +
704
+ PARCEL_TRUST_NOTE +
705
+ " " +
706
+ TRUST_NOTE;
707
+
708
+ // ProofParcel's signed-container framing, passed to the GENERIC core. The core owns the envelope machinery
709
+ // (the wrap-don't-edit invariant, the scheme list, signer recovery); this object supplies ONLY ProofParcel's
710
+ // kind/schema/note + the "signed parcel attestation" label and the UNSIGNED-payload codec the core
711
+ // re-validates the embedded payload with (so the core stays product-agnostic — no back-edge).
712
+ const SIGNED_PARCEL_ATTESTATION_CFG = Object.freeze({
713
+ kind: SIGNED_PARCEL_ATTESTATION_KIND,
714
+ schemaVersion: SIGNED_PARCEL_ATTESTATION_SCHEMA_VERSION,
715
+ supportedSchemaVersions: SUPPORTED_SIGNED_PARCEL_ATTESTATION_SCHEMA_VERSIONS,
716
+ note: SIGNED_PARCEL_ATTESTATION_TRUST_NOTE,
717
+ label: "signed parcel attestation",
718
+ validateUnsigned: validateParcelAttestation,
719
+ serializeUnsigned: serializeParcelAttestation,
720
+ });
721
+
722
+ /**
723
+ * Strictly validate a parsed SIGNED parcel-attestation container. THIN wrapper over the generic core
724
+ * validator with ProofParcel's framing: the core enforces the shared wrap-don't-edit invariant (re-validate
725
+ * + canonical-byte equality of the embedded UNSIGNED payload via ProofParcel's own
726
+ * validate/serializeParcelAttestation), the scheme list, and the signer/signature shape. A DATASET
727
+ * signed-container (different kind) is REJECTED here. Never half-accepts.
728
+ * @param {any} obj
729
+ * @returns {object} the same object, if valid
730
+ */
731
+ function validateSignedParcelAttestation(obj) {
732
+ return coreAttestation.validateSignedAttestation(obj, SIGNED_PARCEL_ATTESTATION_CFG);
733
+ }
734
+
735
+ /**
736
+ * Assemble + validate a SIGNED parcel-attestation container from a validated UNSIGNED envelope and a
737
+ * detached signature triple. PURE: NO signing, NO key handling — the loop never holds a key. Embeds the
738
+ * EXACT canonical unsigned bytes (serializeParcelAttestation) as a string so the signed-over bytes are
739
+ * unambiguous, attaches { scheme, signer, signature }, and strictly validates.
740
+ * @param {object} params
741
+ * @param {object} params.attestation a validated UNSIGNED parcel-attestation envelope
742
+ * @param {string} params.scheme one of SIGNED_PARCEL_ATTESTATION_SCHEMES (e.g. "eip191-personal-sign")
743
+ * @param {string} params.signer the claimed 0x-address of the signer
744
+ * @param {string} params.signature the 0x-hex detached signature over serializeParcelAttestation(attestation)
745
+ * @returns {object} a validated signed parcel-attestation container
746
+ */
747
+ function buildSignedParcelAttestation(params) {
748
+ return coreAttestation.buildSignedAttestation(params, SIGNED_PARCEL_ATTESTATION_CFG);
749
+ }
750
+
751
+ /**
752
+ * Serialize a SIGNED parcel-attestation container to its canonical, byte-deterministic bytes (fixed
753
+ * top-level + signature-block key order, no insignificant whitespace, a single trailing newline).
754
+ * @param {object} container a validated signed parcel-attestation container
755
+ * @returns {string} the canonical serialization (newline-terminated)
756
+ */
757
+ function serializeSignedParcelAttestation(container) {
758
+ return coreAttestation.serializeSignedAttestation(container, SIGNED_PARCEL_ATTESTATION_CFG);
759
+ }
760
+
761
+ /**
762
+ * Read, parse, and STRICTLY validate the SIGNED parcel-attestation container at `signedPath`. Round-trips
763
+ * with serializeSignedParcelAttestation; a malformed/edited/foreign (e.g. a DATASET signed) container is
764
+ * rejected, never half-accepted. Throws on a missing file or invalid JSON too.
765
+ * @param {string} signedPath
766
+ * @returns {object} the validated container
767
+ */
768
+ function readSignedParcelAttestation(signedPath) {
769
+ return coreAttestation.readSignedAttestation(signedPath, SIGNED_PARCEL_ATTESTATION_CFG);
770
+ }
771
+
772
+ /**
773
+ * Recover the signing address from a signed parcel-attestation container's embedded canonical bytes +
774
+ * signature per the declared scheme. THIN wrapper over the core. Returns the recovered address lowercase.
775
+ * @param {object} container a validated signed parcel-attestation container
776
+ * @returns {string} the recovered signer address, 0x-prefixed lowercase
777
+ */
778
+ function recoverSignedParcelAttestationSigner(container) {
779
+ return coreAttestation.recoverSigner(container);
780
+ }
781
+
782
+ /**
783
+ * Orchestrate `vh parcel attest <manifest> [--json] [--out <p>]`. Reads the parcel manifest via the strict
784
+ * `readParcelManifest`, builds the UNSIGNED parcel-attestation envelope, and emits its canonical bytes.
785
+ * With `--out` it writes those exact bytes to the caller's EXPLICIT path (never cwd) and names the file;
786
+ * without `--out` it prints them to stdout. `--json` is the machine form AND is itself the canonical bytes
787
+ * (so a caller can pipe it straight into a signer). PURELY OFFLINE: no tree, no provider, no key, no network.
788
+ *
789
+ * @param {object} opts
790
+ * @param {string} opts.manifest path to a manifest written by `vh parcel build`
791
+ * @param {boolean}[opts.json] emit the canonical machine form (which is the same canonical bytes)
792
+ * @param {string} [opts.out] write the canonical payload to this explicit path (caller-chosen; never cwd)
793
+ * @param {(s:string)=>void}[opts.stdout] sink for stdout (default process.stdout.write); injectable for tests
794
+ * @returns {{ envelope: object, canonical: string, out: string|null }}
795
+ */
796
+ function runParcelAttest(opts) {
797
+ if (!opts || typeof opts !== "object") throw new Error("runParcelAttest requires options");
798
+ const { manifest: manifestPath } = opts;
799
+ const write = opts.stdout || ((s) => process.stdout.write(s));
800
+ if (!manifestPath) throw new Error("runParcelAttest requires a <manifest> path");
801
+
802
+ // Strict read: a corrupt/edited/foreign manifest (INCLUDING a dataset manifest) is rejected here, never
803
+ // half-accepted, BEFORE any payload is built. The file SET it commits to is the TRUSTED basis.
804
+ const manifest = readParcelManifest(manifestPath);
805
+
806
+ const envelope = buildParcelAttestation(manifest);
807
+ const canonical = serializeParcelAttestation(envelope);
808
+
809
+ let outAbs = null;
810
+ if (opts.out) {
811
+ outAbs = path.resolve(opts.out);
812
+ fs.writeFileSync(outAbs, canonical); // the ONLY side effect — at the caller's explicit path, never cwd
813
+ if (!opts.json) write(`parcel attestation written: ${outAbs}\n`);
814
+ }
815
+
816
+ if (opts.json) {
817
+ // The machine form IS the canonical bytes (so a caller can pipe `--json` straight into a signer).
818
+ write(canonical);
819
+ } else if (!outAbs) {
820
+ write(canonical);
821
+ }
822
+
823
+ return { envelope, canonical, out: outAbs };
824
+ }
825
+
826
+ // =================================================================================================
827
+ // `vh parcel sign <manifest> --key-env <VAR> | --key-file <path> [--out <p>] [--json]` — read a
828
+ // HUMAN-supplied key, sign the UNSIGNED parcel attestation, write the SIGNED container (T-19.2).
829
+ //
830
+ // THIN parallel to `vh dataset sign`: it builds the UNSIGNED parcel-attestation payload via the EXISTING
831
+ // `vh parcel attest` code path (buildParcelAttestation — NO re-implementation), resolves a HUMAN-supplied
832
+ // key into an in-process Wallet via the SHARED `loadSigningWallet`, and signs over the canonical bytes via
833
+ // the SAME T-19.1 `signAttestation` core with ProofParcel's signed-container framing. The loop never
834
+ // generates or holds a key. The container ROUND-TRIPS by construction: `vh parcel verify-attest` recovers
835
+ // exactly this signer over exactly these bytes, and a DATASET signed-container does NOT cross-verify
836
+ // (distinct kind).
837
+ //
838
+ // KEY HYGIENE (load-bearing): EXACTLY ONE of `--key-env`/`--key-file`; neither/both, a missing env var, an
839
+ // unreadable file, or a malformed/zero key HARD-ERRORS BEFORE any signing, with a message that NEVER
840
+ // includes the key. Success/`--json` output prints ONLY the signer ADDRESS, the output path, and the
841
+ // scheme — never the key.
842
+
843
+ // The signing-specific caveat the human-output sign path LEADS with (P-3, verbatim). This signs the parcel
844
+ // IDENTITY with the caller's OWN key; "the signer says so" is NOT a trusted delivery TIMESTAMP.
845
+ const SIGN_TRUST_NOTE =
846
+ "This signs the parcel IDENTITY (root, fileCount, manifestDigest) with the key YOU supplied. A " +
847
+ "self-managed key attests \"the signer says so\" — it is NOT an independent, trusted TIMESTAMP: " +
848
+ '"delivered/unaltered since a date T" still needs the human-owned signing/timestamp trust-root ' +
849
+ "(needs-human, P-3). The key must be one YOU provisioned OUTSIDE this tool.";
850
+
851
+ /**
852
+ * Orchestrate `vh parcel sign <manifest> --key-env <VAR> | --key-file <path> [--out <p>] [--json]`. Reads
853
+ * the parcel manifest via the strict `readParcelManifest`, builds the UNSIGNED attestation via the EXISTING
854
+ * `buildParcelAttestation` path (NO re-implementation), resolves a HUMAN-supplied key into an in-process
855
+ * Wallet via the shared `loadSigningWallet`, signs over the canonical bytes via the T-19.1 `signAttestation`
856
+ * core, and writes the SIGNED container's canonical bytes to `--out` (or stdout). PURELY OFFLINE.
857
+ *
858
+ * KEY HYGIENE: the key is read, used, and discarded; NEVER returned, persisted, or logged. The
859
+ * success/`--json` output prints ONLY the signer address, the output path, and the scheme — never the key.
860
+ *
861
+ * @param {object} opts
862
+ * @param {string} opts.manifest path to a manifest written by `vh parcel build`
863
+ * @param {string} [opts.keyEnv] env var holding the signing key (EXACTLY ONE of keyEnv/keyFile)
864
+ * @param {string} [opts.keyFile] path to a key file the human created (EXACTLY ONE of keyEnv/keyFile)
865
+ * @param {boolean}[opts.json] emit a machine-readable { signer, out, scheme, container, ... } object;
866
+ * with NO --out the `container` field carries the canonical signed bytes so
867
+ * `--json` never silently drops the artifact (parity with `attest --json`)
868
+ * @param {string} [opts.out] write the signed container to this explicit path (caller-chosen; never cwd)
869
+ * @param {(s:string)=>void}[opts.stdout] sink for stdout (default process.stdout.write); injectable for tests
870
+ * @returns {Promise<{ container: object, canonical: string, signer: string, scheme: string, out: string|null }>}
871
+ */
872
+ async function runParcelSign(opts) {
873
+ if (!opts || typeof opts !== "object") throw new Error("runParcelSign requires options");
874
+ const { manifest: manifestPath, keyEnv, keyFile } = opts;
875
+ const write = opts.stdout || ((s) => process.stdout.write(s));
876
+ if (!manifestPath) throw new Error("runParcelSign requires a <manifest> path");
877
+
878
+ // Resolve the HUMAN-supplied key into an in-process Wallet FIRST (BEFORE any signing). Neither/both
879
+ // sources, a missing env var, an unreadable file, or a malformed/zero key hard-errors here with a
880
+ // key-free message.
881
+ const { wallet } = coreAttestation.loadSigningWallet({ keyEnv, keyFile });
882
+
883
+ // Strict read: a corrupt/edited/foreign manifest (INCLUDING a dataset manifest) is rejected here, never
884
+ // half-accepted. The file SET it commits to is the TRUSTED basis of the attestation identity.
885
+ const manifest = readParcelManifest(manifestPath);
886
+
887
+ // Build the UNSIGNED payload via the EXISTING `vh parcel attest` path (NO re-implementation), then route
888
+ // the Wallet + payload through the SAME T-19.1 core with ProofParcel's signed-container framing.
889
+ const unsigned = buildParcelAttestation(manifest);
890
+ const container = await coreAttestation.signAttestation(
891
+ { attestation: unsigned, signer: wallet },
892
+ SIGNED_PARCEL_ATTESTATION_CFG
893
+ );
894
+ const canonical = serializeSignedParcelAttestation(container);
895
+ const signer = container.signature.signer; // lowercase 0x-address (PUBLIC) — never the key
896
+ const scheme = container.signature.scheme;
897
+
898
+ let outAbs = null;
899
+ if (opts.out) {
900
+ // Write the EXACT canonical signed bytes to the caller-chosen path (resolved absolute) — never cwd.
901
+ // The ONLY side effect. NOTHING about the key is written.
902
+ outAbs = path.resolve(opts.out);
903
+ fs.writeFileSync(outAbs, canonical);
904
+ }
905
+
906
+ if (opts.json) {
907
+ // Machine form: ONLY public fields — signer ADDRESS, output path, scheme. NEVER the key.
908
+ //
909
+ // ARTIFACT PARITY with `attest --json` (which emits the canonical bytes on stdout so a caller can
910
+ // pipe straight on). When there is NO --out, the signed container has nowhere else to live, so we
911
+ // carry the EXACT canonical signed bytes in a `container` field — `--json` without --out NEVER drops
912
+ // the artifact. With --out the bytes are on disk at `out`, so `container` is null (no redundant copy).
913
+ write(
914
+ JSON.stringify({
915
+ signed: true,
916
+ signer,
917
+ scheme,
918
+ out: outAbs,
919
+ kind: container.kind,
920
+ // The canonical signed bytes when there is no file to point at; null when --out holds them.
921
+ container: outAbs ? null : canonical,
922
+ note: SIGN_TRUST_NOTE,
923
+ }) + "\n"
924
+ );
925
+ } else {
926
+ write(` TRUST: ${SIGN_TRUST_NOTE}\n`);
927
+ // The success line names WHICH key signed (by its PUBLIC address) so the human can confirm.
928
+ write(`signed by ${signer}\n`);
929
+ write(` scheme: ${scheme}\n`);
930
+ if (outAbs) {
931
+ write(` signed parcel attestation written: ${outAbs}\n`);
932
+ } else {
933
+ write(canonical);
934
+ }
935
+ }
936
+
937
+ return { container, canonical, signer, scheme, out: outAbs };
938
+ }
939
+
940
+ // =================================================================================================
941
+ // `vh parcel verify-attest <signed> [--manifest <m>] [--signer <addr>] [--json]` — the OFFLINE verifier
942
+ // for a SIGNED parcel-attestation container, over the SAME generic core `vh dataset verify-attest` uses.
943
+ //
944
+ // WHY THIS EXISTS
945
+ // A recipient handed a "signed by the sender" parcel attestation needs ONE command that answers, with no
946
+ // key and no network: (1) is the embedded signature genuine — does it recover to the address the container
947
+ // CLAIMS as `signer`? (2) Optionally: is the recovered signer the SPECIFIC sender I expected (`--signer`)?
948
+ // (3) Optionally: does the signature bind the parcel I actually hold (`--manifest`)? — by recomputing the
949
+ // canonical UNSIGNED bytes from MY parcel manifest via the EXISTING build path and requiring them
950
+ // byte-identical to the embedded payload.
951
+ //
952
+ // PURELY OFFLINE: no tree walk, no provider, no key, no network. EXIT CODES mirror the family's 0/3
953
+ // data-divergence convention: 0 ACCEPTED, 3 REJECTED (so a recipient's CI can gate "genuinely signed by
954
+ // our sender and binds this parcel"), 2 usage error, 1 runtime error.
955
+ //
956
+ // TRUST POSTURE (verbatim into output). A valid signature proves the HOLDER OF `signer`'s KEY vouched for
957
+ // THIS parcel identity. It does NOT by itself prove a trustworthy delivery TIMESTAMP ("delivered on date
958
+ // T" still rides the human-owned trust-root, P-3), and the `parcel` block (parcelId/sender/recipient) is
959
+ // UNTRUSTED self-asserted metadata. Never overclaims past P-3.
960
+
961
+ const VERIFY_ATTEST_VERDICT = Object.freeze({ ACCEPTED: "ACCEPTED", REJECTED: "REJECTED" });
962
+
963
+ // The standing trust caveat the verify-attest output LEADS with. REUSES the shared TRUST_NOTE verbatim
964
+ // (so the family caveats never drift) + the parcel-specific caveat + the signing-specific caveat: a valid
965
+ // signature proves the key-holder vouched for this parcel IDENTITY; it does NOT prove a delivery timestamp
966
+ // (P-3, needs-human). Never overclaims.
967
+ const PARCEL_VERIFY_ATTEST_TRUST_NOTE =
968
+ "A valid signature proves the HOLDER OF `signer`'s key vouched for THIS parcel identity (the embedded " +
969
+ "root, fileCount, manifestDigest). It does NOT by itself prove a trustworthy delivery TIMESTAMP: " +
970
+ '"delivered/unaltered since a date T" still needs the human-owned signing/timestamp trust-root ' +
971
+ "(needs-human, P-3). " +
972
+ PARCEL_TRUST_NOTE +
973
+ " " +
974
+ TRUST_NOTE;
975
+
976
+ /**
977
+ * Verify (purely, OFFLINE) a signed parcel-attestation container: recover the signer and confirm it equals
978
+ * the container's CLAIMED `signer`; OPTIONALLY pin it to an EXPECTED sender (`expectedSigner`); OPTIONALLY
979
+ * confirm the signature binds a recipient's own parcel (`manifest`) by recomputing the canonical UNSIGNED
980
+ * bytes via the EXISTING build path and requiring them byte-identical to the embedded payload. The verdict
981
+ * is ACCEPTED only when EVERY requested check passes. No I/O, no provider, no key, no network. The ONLY
982
+ * ProofParcel-specific step is recomputing the canonical bytes from the recipient's own parcel manifest;
983
+ * the recovery / claimed-signer / expected-signer / binding logic is the GENERIC core.
984
+ * @param {object} params
985
+ * @param {object} params.container a validated signed parcel-attestation container
986
+ * @param {string} [params.expectedSigner] OPTIONAL expected sender 0x-address (--signer); checked when present
987
+ * @param {object} [params.manifest] OPTIONAL validated parcel-manifest object; binding check when present
988
+ * @returns {object} the object the core verifySignedAttestation returns (shape parity with dataset verify-attest)
989
+ */
990
+ function verifySignedParcelAttestation(params) {
991
+ if (!params || typeof params !== "object") {
992
+ throw new Error("verifySignedParcelAttestation requires { container, [expectedSigner], [manifest] }");
993
+ }
994
+ const { container, expectedSigner, manifest } = params;
995
+ let expectedCanonical;
996
+ if (manifest !== undefined && manifest !== null) {
997
+ expectedCanonical = serializeParcelAttestation(buildParcelAttestation(manifest));
998
+ }
999
+ return coreAttestation.verifySignedAttestation({ container, expectedSigner, expectedCanonical });
1000
+ }
1001
+
1002
+ /**
1003
+ * Render a parcel verify-attest result as the human-readable block the CLI prints. LEADS with the standing
1004
+ * trust caveat (reuses TRUST_NOTE + PARCEL_TRUST_NOTE + the signing caveat — never overclaims past P-3),
1005
+ * then the verdict, the recovered/claimed/expected signer, and each requested check with PASS/FAIL.
1006
+ * @param {object} r the object verifySignedParcelAttestation returns
1007
+ * @returns {string[]} lines
1008
+ */
1009
+ function formatParcelVerifyAttest(r) {
1010
+ const lines = [
1011
+ " TRUST: " + PARCEL_VERIFY_ATTEST_TRUST_NOTE,
1012
+ "",
1013
+ ` verify-attest: ${r.verdict}`,
1014
+ ` scheme: ${r.scheme}`,
1015
+ ` recovered signer: ${r.recoveredSigner} (from the embedded canonical bytes + signature)`,
1016
+ ` claimed signer: ${r.claimedSigner} (the container's \`signer\` field)`,
1017
+ ];
1018
+ lines.push(
1019
+ ` [${r.checks.signatureMatchesSigner ? "PASS" : "FAIL"}] signature recovers to the claimed signer`
1020
+ );
1021
+ if (r.checks.signerMatchesExpected === null) {
1022
+ lines.push(" [skip] expected-signer pin: not requested (pass --signer <addr> to pin the sender)");
1023
+ } else {
1024
+ lines.push(
1025
+ ` [${r.checks.signerMatchesExpected ? "PASS" : "FAIL"}] recovered signer matches the expected ` +
1026
+ `sender (${r.expectedSigner})`
1027
+ );
1028
+ }
1029
+ if (r.checks.manifestBindsAttestation === null) {
1030
+ lines.push(
1031
+ " [skip] parcel binding: not requested (pass --manifest <m> to bind the signature to YOUR parcel)"
1032
+ );
1033
+ } else {
1034
+ lines.push(
1035
+ ` [${r.checks.manifestBindsAttestation ? "PASS" : "FAIL"}] the signature binds YOUR parcel ` +
1036
+ "(its canonical bytes are byte-identical to the signed payload)"
1037
+ );
1038
+ }
1039
+ if (r.accepted) {
1040
+ lines.push(" ACCEPTED: every requested check passed.");
1041
+ } else {
1042
+ lines.push(` REJECTED: failed check(s): ${r.failedChecks.join(", ")}.`);
1043
+ if (r.failedChecks.includes("manifestBindsAttestation")) {
1044
+ lines.push(
1045
+ " binding-mismatch: the signed payload does NOT match YOUR parcel — the signature vouches for a"
1046
+ );
1047
+ lines.push(" DIFFERENT parcel identity than the one you hold.");
1048
+ }
1049
+ }
1050
+ return lines;
1051
+ }
1052
+
1053
+ /**
1054
+ * Orchestrate `vh parcel verify-attest <signed> [--manifest <m>] [--signer <addr>] [--json]`. Reads the
1055
+ * signed container via the strict `readSignedParcelAttestation` (a malformed/edited/foreign — e.g. a DATASET
1056
+ * signed — container is rejected, never half-accepted) and, when given, the recipient's manifest via the
1057
+ * strict `readParcelManifest`, then runs the PURE `verifySignedParcelAttestation`. Emits the verdict as a
1058
+ * human block (LEADS with the trust caveat) or a `--json` machine-readable object. PURELY OFFLINE.
1059
+ *
1060
+ * @param {object} opts
1061
+ * @param {string} opts.signed path to a signed parcel-attestation container
1062
+ * @param {string} [opts.manifest] OPTIONAL path to the recipient's parcel manifest (binds the signature to it)
1063
+ * @param {string} [opts.signer] OPTIONAL expected sender 0x-address to pin
1064
+ * @param {boolean}[opts.json] emit the machine-readable verdict instead of the human block
1065
+ * @param {(s:string)=>void}[opts.stdout] sink for stdout (default process.stdout.write); injectable for tests
1066
+ * @returns {object} the object verifySignedParcelAttestation returns
1067
+ */
1068
+ function runParcelVerifyAttest(opts) {
1069
+ if (!opts || typeof opts !== "object") throw new Error("runParcelVerifyAttest requires options");
1070
+ const { signed: signedPath, manifest: manifestPath, signer: expectedSigner } = opts;
1071
+ const write = opts.stdout || ((s) => process.stdout.write(s));
1072
+ if (!signedPath) throw new Error("runParcelVerifyAttest requires a <signed> path");
1073
+
1074
+ // Strict read: a malformed/edited/foreign signed container is rejected here, never half-accepted, BEFORE
1075
+ // any recovery is attempted. (Also re-validates the embedded UNSIGNED payload, scheme, signer, sig shape.)
1076
+ const container = readSignedParcelAttestation(signedPath);
1077
+
1078
+ // OPTIONAL: read the recipient's parcel manifest strictly so the binding check recomputes canonical bytes
1079
+ // from a sound manifest (a corrupt/foreign manifest is rejected).
1080
+ let manifest;
1081
+ if (manifestPath !== undefined && manifestPath !== null) {
1082
+ manifest = readParcelManifest(manifestPath);
1083
+ }
1084
+
1085
+ let result = verifySignedParcelAttestation({ container, expectedSigner, manifest });
1086
+
1087
+ // OPTIONAL recipient-side TRUST-DECISION-AS-OF (EPIC-51 / T-51.2). Runs ONLY under --revocations — with no
1088
+ // flag `result` is byte-identical to the pre-EPIC baseline. A sender key revoked-before-as-of downgrades an
1089
+ // otherwise-ACCEPTED parcel attestation to REVOKED (accepted:false => exit 3 via the caller's `accepted ? 0
1090
+ // : 3` mapping); a later revocation is informational; a forged one is ignored with a warning. OFFLINE /
1091
+ // key-free on the read side. The revocations file is the ONLY new I/O.
1092
+ let defaulted = false;
1093
+ if (opts.revocations) {
1094
+ const applied = coreTrustAsOf.loadAndApply({
1095
+ result,
1096
+ revocationsPath: opts.revocations,
1097
+ asOf: opts.asOf,
1098
+ nowISO: opts.nowISO || new Date().toISOString(),
1099
+ readFile: (p) => fs.readFileSync(path.resolve(p), "utf8"),
1100
+ });
1101
+ result = applied.result;
1102
+ defaulted = applied.defaulted;
1103
+ }
1104
+
1105
+ if (opts.json) {
1106
+ write(JSON.stringify(result) + "\n");
1107
+ } else {
1108
+ for (const line of formatParcelVerifyAttest(result)) write(line + "\n");
1109
+ if (result.trustAsOf) {
1110
+ for (const line of coreTrustAsOf.renderTrustAsOf(result.trustAsOf, { indent: " ", defaulted })) {
1111
+ write(line + "\n");
1112
+ }
1113
+ }
1114
+ }
1115
+ return result;
1116
+ }
1117
+
1118
+ // =================================================================================================
1119
+ // DETACHED TIMESTAMP container for ProofParcel (T-20.2) — an INDEPENDENT RFC-3161 TSA timestamp WRAPPED
1120
+ // AROUND the canonical UNSIGNED parcel attestation, over the SAME generic timestamp core `vh dataset` uses.
1121
+ // ProofParcel's OWN container `kind` means a DATASET timestamped container does NOT cross-validate as a
1122
+ // parcel one (and vice-versa) even though the UNSIGNED identity bytes can coincide for the same files.
1123
+ //
1124
+ // THE DIGEST IS SHA-256 — NOT the keccak256 manifestDigest (universal across TSAs; see the dataset section).
1125
+
1126
+ const TIMESTAMPED_PARCEL_ATTESTATION_KIND = "verifyhash.parcel-attestation-timestamped";
1127
+ const TIMESTAMPED_PARCEL_ATTESTATION_SCHEMA_VERSION = 1;
1128
+ const SUPPORTED_TIMESTAMPED_PARCEL_ATTESTATION_SCHEMA_VERSIONS = Object.freeze([1]);
1129
+
1130
+ // The standing trust caveat carried IN-BAND in every timestamped parcel container. REUSES the shared
1131
+ // TRUST_NOTE + the parcel caveat verbatim, and adds ONLY the timestamp-specific caveat: a token attests an
1132
+ // INDEPENDENT TSA saw this digest by genTime — to the strength of the TSA you TRUST; the loop does NOT
1133
+ // validate the TSA cert chain / CMS signature (human out-of-band trust anchor).
1134
+ const TIMESTAMPED_PARCEL_ATTESTATION_TRUST_NOTE =
1135
+ "This is a TIMESTAMPED parcel attestation container: it wraps (never edits) the EXACT canonical UNSIGNED " +
1136
+ "parcel-attestation bytes in `attestation` and attaches an RFC-3161 timestamp token over the SHA-256 " +
1137
+ "digest of those exact bytes. It asserts that an INDEPENDENT Time-Stamping Authority (TSA) saw THIS " +
1138
+ "digest by the token's genTime — to the strength of the TSA you TRUST. It does NOT validate the TSA's " +
1139
+ "certificate chain or the token's CMS signature (verify those out-of-band, e.g. `openssl ts -verify`). " +
1140
+ "The digest is a STANDARD sha256(canonical attestation bytes) — NOT the project's internal keccak256 " +
1141
+ "manifestDigest. Every caveat of the embedded UNSIGNED payload still applies. " +
1142
+ PARCEL_TRUST_NOTE +
1143
+ " " +
1144
+ TRUST_NOTE;
1145
+
1146
+ // ProofParcel's timestamp-container framing, passed to the GENERIC timestamp core (no back-edge).
1147
+ const TIMESTAMPED_PARCEL_ATTESTATION_CFG = Object.freeze({
1148
+ kind: TIMESTAMPED_PARCEL_ATTESTATION_KIND,
1149
+ schemaVersion: TIMESTAMPED_PARCEL_ATTESTATION_SCHEMA_VERSION,
1150
+ supportedSchemaVersions: SUPPORTED_TIMESTAMPED_PARCEL_ATTESTATION_SCHEMA_VERSIONS,
1151
+ note: TIMESTAMPED_PARCEL_ATTESTATION_TRUST_NOTE,
1152
+ label: "timestamped parcel attestation",
1153
+ validateUnsigned: validateParcelAttestation,
1154
+ serializeUnsigned: serializeParcelAttestation,
1155
+ });
1156
+
1157
+ function validateTimestampedParcelAttestation(obj) {
1158
+ return coreTimestamp.validateTimestampContainer(obj, TIMESTAMPED_PARCEL_ATTESTATION_CFG);
1159
+ }
1160
+ function buildTimestampedParcelAttestation(params) {
1161
+ return coreTimestamp.buildTimestampContainer(params, TIMESTAMPED_PARCEL_ATTESTATION_CFG);
1162
+ }
1163
+ function serializeTimestampedParcelAttestation(container) {
1164
+ return coreTimestamp.serializeTimestampContainer(container, TIMESTAMPED_PARCEL_ATTESTATION_CFG);
1165
+ }
1166
+ function readTimestampedParcelAttestation(containerPath) {
1167
+ return coreTimestamp.readTimestampContainer(containerPath, TIMESTAMPED_PARCEL_ATTESTATION_CFG);
1168
+ }
1169
+
1170
+ // The parcel timestamp-request human note (parallel to the dataset one).
1171
+ const PARCEL_TIMESTAMP_REQUEST_TRUST_NOTE =
1172
+ "This emits the SHA-256 digest of the canonical UNSIGNED parcel-attestation bytes — the EXACT digest you " +
1173
+ "submit to your RFC-3161 Time-Stamping Authority (TSA). A timestamp token will attest an INDEPENDENT TSA " +
1174
+ "saw THIS digest by its genTime — to the strength of the TSA you TRUST; this tool does NOT obtain the " +
1175
+ "token (a human/network step) and does NOT validate the TSA cert chain. The digest is a STANDARD SHA-256 " +
1176
+ "(universal across TSAs) — NOT the project's internal keccak256 manifestDigest.";
1177
+
1178
+ function parcelTimestampRequestRecipe(digestHex) {
1179
+ return [
1180
+ " To obtain an RFC-3161 timestamp token over this digest (a HUMAN/network step):",
1181
+ ` openssl ts -query -digest ${digestHex} -sha256 -cert -out request.tsq`,
1182
+ " # send request.tsq to your TSA (e.g. `curl` to its HTTP endpoint) -> response.tsr",
1183
+ " openssl ts -reply -in response.tsr -token_out -out token.der",
1184
+ " Then wrap it back into a verifiable container (no key, no network):",
1185
+ " vh parcel timestamp-wrap <manifest> --token token.der --out attestation.timestamped.json",
1186
+ ];
1187
+ }
1188
+
1189
+ /**
1190
+ * Orchestrate `vh parcel timestamp-request <manifest> [--out <p>] [--json]` — THIN parallel to the dataset
1191
+ * command, over the parcel attest build path. Emits the SHA-256 digest the human submits to their TSA plus
1192
+ * a recipe. PURELY OFFLINE: NO key, NO network.
1193
+ * @param {object} opts { manifest, json?, out?, stdout? }
1194
+ * @returns {{ digest: string, hashAlgorithm: string, canonical: string, out: string|null }}
1195
+ */
1196
+ function runParcelTimestampRequest(opts) {
1197
+ if (!opts || typeof opts !== "object") throw new Error("runParcelTimestampRequest requires options");
1198
+ const { manifest: manifestPath } = opts;
1199
+ const write = opts.stdout || ((s) => process.stdout.write(s));
1200
+ if (!manifestPath) throw new Error("runParcelTimestampRequest requires a <manifest> path");
1201
+
1202
+ const manifest = readParcelManifest(manifestPath);
1203
+ const canonical = serializeParcelAttestation(buildParcelAttestation(manifest));
1204
+ const digest = coreTimestamp.sha256Hex(canonical);
1205
+
1206
+ let outAbs = null;
1207
+ if (opts.out) {
1208
+ outAbs = path.resolve(opts.out);
1209
+ fs.writeFileSync(
1210
+ outAbs,
1211
+ JSON.stringify(
1212
+ {
1213
+ kind: "verifyhash.timestamp-request",
1214
+ hashAlgorithm: "sha256",
1215
+ digest,
1216
+ attestation: canonical,
1217
+ note: PARCEL_TIMESTAMP_REQUEST_TRUST_NOTE,
1218
+ },
1219
+ null,
1220
+ 2
1221
+ ) + "\n"
1222
+ );
1223
+ }
1224
+
1225
+ if (opts.json) {
1226
+ write(
1227
+ JSON.stringify({
1228
+ hashAlgorithm: "sha256",
1229
+ digest,
1230
+ canonical,
1231
+ out: outAbs,
1232
+ note: PARCEL_TIMESTAMP_REQUEST_TRUST_NOTE,
1233
+ }) + "\n"
1234
+ );
1235
+ } else {
1236
+ write(` TRUST: ${PARCEL_TIMESTAMP_REQUEST_TRUST_NOTE}\n`);
1237
+ write("\n");
1238
+ write(` sha256 digest (the messageImprint to stamp): ${digest}\n`);
1239
+ write("\n");
1240
+ for (const line of parcelTimestampRequestRecipe(digest)) write(line + "\n");
1241
+ if (outAbs) write(` timestamp request written: ${outAbs}\n`);
1242
+ }
1243
+ return { digest, hashAlgorithm: "sha256", canonical, out: outAbs };
1244
+ }
1245
+
1246
+ const PARCEL_TIMESTAMP_WRAP_TRUST_NOTE = TIMESTAMPED_PARCEL_ATTESTATION_TRUST_NOTE;
1247
+
1248
+ function resolveParcelTimestampToken(tokenArg) {
1249
+ if (typeof tokenArg !== "string" || tokenArg.length === 0) {
1250
+ throw new Error("--token requires a path to an RFC-3161 token file OR an inline base64 token");
1251
+ }
1252
+ if (fs.existsSync(tokenArg)) {
1253
+ return fs.readFileSync(tokenArg);
1254
+ }
1255
+ return tokenArg;
1256
+ }
1257
+
1258
+ /**
1259
+ * Orchestrate `vh parcel timestamp-wrap <manifest> --token <path|base64> [--out <p>] [--json]` — THIN
1260
+ * parallel to the dataset command, over the parcel attest build path + ProofParcel's container framing.
1261
+ * ERRORS CLEARLY if the token does not bind the re-derived SHA-256 digest. PURELY OFFLINE: NO key, NO network.
1262
+ * @param {object} opts { manifest, token, json?, out?, stdout? }
1263
+ * @returns {{ container: object, canonical: string, digest: string, genTime: string, out: string|null }}
1264
+ */
1265
+ function runParcelTimestampWrap(opts) {
1266
+ if (!opts || typeof opts !== "object") throw new Error("runParcelTimestampWrap requires options");
1267
+ const { manifest: manifestPath, token: tokenArg } = opts;
1268
+ const write = opts.stdout || ((s) => process.stdout.write(s));
1269
+ if (!manifestPath) throw new Error("runParcelTimestampWrap requires a <manifest> path");
1270
+ if (!tokenArg) throw new Error("runParcelTimestampWrap requires a --token <path|base64>");
1271
+
1272
+ const manifest = readParcelManifest(manifestPath);
1273
+ const unsigned = buildParcelAttestation(manifest);
1274
+ const token = resolveParcelTimestampToken(tokenArg);
1275
+
1276
+ const container = buildTimestampedParcelAttestation({ attestation: unsigned, token });
1277
+ const canonical = serializeTimestampedParcelAttestation(container);
1278
+ const facts = coreTimestamp.readTimestampFacts(container);
1279
+
1280
+ let outAbs = null;
1281
+ if (opts.out) {
1282
+ outAbs = path.resolve(opts.out);
1283
+ fs.writeFileSync(outAbs, canonical);
1284
+ }
1285
+
1286
+ if (opts.json) {
1287
+ write(
1288
+ JSON.stringify({
1289
+ kind: container.kind,
1290
+ scheme: container.timestamp.scheme,
1291
+ hashAlgorithm: container.timestamp.hashAlgorithm,
1292
+ digest: facts.digest,
1293
+ genTime: facts.genTime,
1294
+ serialNumber: facts.serialNumber,
1295
+ policyOID: facts.policyOID,
1296
+ out: outAbs,
1297
+ container: outAbs ? null : canonical,
1298
+ note: PARCEL_TIMESTAMP_WRAP_TRUST_NOTE,
1299
+ }) + "\n"
1300
+ );
1301
+ } else {
1302
+ write(` TRUST: ${PARCEL_TIMESTAMP_WRAP_TRUST_NOTE}\n`);
1303
+ write("\n");
1304
+ write(` timestamped: an INDEPENDENT TSA stamped this digest by genTime\n`);
1305
+ write(` digest (sha256 of the canonical attestation bytes): ${facts.digest}\n`);
1306
+ write(` genTime (asserted by the TSA): ${facts.genTime}\n`);
1307
+ write(` TSA serial: ${facts.serialNumber.hex}\n`);
1308
+ write(` policy OID: ${facts.policyOID}\n`);
1309
+ if (outAbs) {
1310
+ write(` timestamped parcel attestation written: ${outAbs}\n`);
1311
+ } else {
1312
+ write(canonical);
1313
+ }
1314
+ }
1315
+ return { container, canonical, digest: facts.digest, genTime: facts.genTime, out: outAbs };
1316
+ }
1317
+
1318
+ // =================================================================================================
1319
+ // `vh parcel verify-timestamp <container> [--manifest <m>] [--json]` — the OFFLINE independent-timestamp
1320
+ // verifier for ProofParcel (T-20.3, EPIC-20). THIN parallel to `vh dataset verify-timestamp`, over the SAME
1321
+ // generic timestamp core. ProofParcel's OWN container `kind` means a DATASET timestamped container does NOT
1322
+ // cross-validate here (and vice versa) — the strict validator rejects the wrong kind.
1323
+
1324
+ const PARCEL_VERIFY_TIMESTAMP_VERDICT = coreTimestamp.VERIFY_TIMESTAMP_VERDICT;
1325
+
1326
+ // The bounded, honest claim the parcel verify-timestamp output LEADS with. REUSES the shared TRUST_NOTE +
1327
+ // the parcel caveat verbatim, and states EXACTLY what ACCEPTED means: an RFC-3161 TSA asserted this parcel
1328
+ // identity existed by genTime, to the strength of the TSA YOU trust; this command does NOT validate the
1329
+ // TSA cert chain / CMS signature (use a CMS verifier / `openssl ts -verify`). Never "delivered since T"
1330
+ // unqualified.
1331
+ const PARCEL_VERIFY_TIMESTAMP_TRUST_NOTE =
1332
+ "ACCEPTED means an RFC-3161 Time-Stamping Authority (TSA) asserted this exact parcel identity (the " +
1333
+ "SHA-256 digest of the canonical attestation bytes) existed by the asserted genTime. This is as " +
1334
+ "trustworthy as the TSA whose certificate YOU trust — this command does NOT validate the TSA's " +
1335
+ "certificate chain or the token's CMS signature (use your platform's CMS verifier, e.g. " +
1336
+ "`openssl ts -verify`, for full PKI validation). It NEVER claims \"delivered/unaltered since date T\" " +
1337
+ "without that qualification. The digest is a STANDARD sha256(canonical attestation bytes) — NOT the " +
1338
+ "project's internal keccak256 manifestDigest. " +
1339
+ PARCEL_TRUST_NOTE +
1340
+ " " +
1341
+ TRUST_NOTE;
1342
+
1343
+ /**
1344
+ * Verify (purely, OFFLINE) a TIMESTAMPED parcel-attestation container. THIN wrapper over the generic core
1345
+ * verifier with ProofParcel's framing. When `manifest` is given, re-derives the recipient's OWN canonical
1346
+ * UNSIGNED bytes via the EXISTING build path and requires the embedded attestation to match byte-for-byte.
1347
+ * @param {object} params { container, [manifest] }
1348
+ * @returns {object} the object the core verifyTimestampContainer returns
1349
+ */
1350
+ function verifyTimestampedParcelAttestation(params) {
1351
+ if (!params || typeof params !== "object") {
1352
+ throw new Error("verifyTimestampedParcelAttestation requires { container, [manifest] }");
1353
+ }
1354
+ const { container, manifest } = params;
1355
+ let expectedManifestCanonical;
1356
+ if (manifest !== undefined && manifest !== null) {
1357
+ expectedManifestCanonical = serializeParcelAttestation(buildParcelAttestation(manifest));
1358
+ }
1359
+ return coreTimestamp.verifyTimestampContainer(
1360
+ { container, expectedManifestCanonical },
1361
+ TIMESTAMPED_PARCEL_ATTESTATION_CFG
1362
+ );
1363
+ }
1364
+
1365
+ /**
1366
+ * Render a parcel verify-timestamp result as the human-readable block the CLI prints. LEADS with the
1367
+ * bounded trust claim, then the verdict, the asserted genTime / TSA serial / policy OID (on ACCEPTED), and
1368
+ * each requested check with PASS/FAIL. A REJECTED verdict NAMES which check failed.
1369
+ * @param {object} r the object verifyTimestampedParcelAttestation returns
1370
+ * @returns {string[]} lines
1371
+ */
1372
+ function formatParcelVerifyTimestamp(r) {
1373
+ const lines = [
1374
+ " TRUST: " + PARCEL_VERIFY_TIMESTAMP_TRUST_NOTE,
1375
+ "",
1376
+ ` verify-timestamp: ${r.verdict}`,
1377
+ ];
1378
+ lines.push(
1379
+ ` [${r.checks.structureAndBinding ? "PASS" : "FAIL"}] the token binds sha256(canonical attestation ` +
1380
+ "bytes) under RFC-3161 (structure + digest + messageImprint)"
1381
+ );
1382
+ if (r.checks.manifestBindsAttestation === null) {
1383
+ lines.push(
1384
+ " [skip] parcel binding: not requested (pass --manifest <m> to bind the timestamp to YOUR parcel)"
1385
+ );
1386
+ } else {
1387
+ lines.push(
1388
+ ` [${r.checks.manifestBindsAttestation ? "PASS" : "FAIL"}] the timestamp binds YOUR manifest ` +
1389
+ "(its canonical bytes are byte-identical to the timestamped payload)"
1390
+ );
1391
+ }
1392
+ if (r.accepted) {
1393
+ lines.push(" ACCEPTED: an RFC-3161 TSA asserted this parcel identity existed by:");
1394
+ lines.push(` genTime (ISO UTC): ${r.genTime}`);
1395
+ lines.push(` TSA serialNumber: ${r.serialNumber.hex} (decimal ${r.serialNumber.decimal})`);
1396
+ lines.push(` policy OID: ${r.policyOID}`);
1397
+ lines.push(` digest (sha256): ${r.digest}`);
1398
+ } else {
1399
+ lines.push(` REJECTED: failed check(s): ${r.failedChecks.join(", ")}.`);
1400
+ if (r.reason) lines.push(` reason: ${r.reason}`);
1401
+ }
1402
+ return lines;
1403
+ }
1404
+
1405
+ /**
1406
+ * Orchestrate `vh parcel verify-timestamp <container> [--manifest <m>] [--json]`. THIN parallel to the
1407
+ * dataset one: reads the raw JSON at the I/O boundary (missing/non-JSON -> runtime error), runs the PURE
1408
+ * verifier (a tampered-but-parseable container is a clean NAMED REJECTED), and optionally binds to the
1409
+ * recipient's own manifest. PURELY OFFLINE: NO key, NO network.
1410
+ * @param {object} opts { container, [manifest], [json], [stdout] }
1411
+ * @returns {object} the object verifyTimestampedParcelAttestation returns
1412
+ */
1413
+ function runParcelVerifyTimestamp(opts) {
1414
+ if (!opts || typeof opts !== "object") throw new Error("runParcelVerifyTimestamp requires options");
1415
+ const { container: containerPath, manifest: manifestPath } = opts;
1416
+ const write = opts.stdout || ((s) => process.stdout.write(s));
1417
+ if (!containerPath) throw new Error("runParcelVerifyTimestamp requires a <container> path");
1418
+
1419
+ let raw;
1420
+ try {
1421
+ raw = fs.readFileSync(containerPath, "utf8");
1422
+ } catch (e) {
1423
+ throw new Error(`cannot read timestamped parcel attestation at ${containerPath}: ${e.message}`);
1424
+ }
1425
+ let container;
1426
+ try {
1427
+ container = JSON.parse(raw);
1428
+ } catch (e) {
1429
+ throw new Error(`timestamped parcel attestation at ${containerPath} is not valid JSON: ${e.message}`);
1430
+ }
1431
+
1432
+ let manifest;
1433
+ if (manifestPath !== undefined && manifestPath !== null) {
1434
+ manifest = readParcelManifest(manifestPath);
1435
+ }
1436
+
1437
+ const result = verifyTimestampedParcelAttestation({ container, manifest });
1438
+
1439
+ if (opts.json) {
1440
+ write(JSON.stringify(result) + "\n");
1441
+ } else {
1442
+ for (const line of formatParcelVerifyTimestamp(result)) write(line + "\n");
1443
+ }
1444
+ return result;
1445
+ }
1446
+
1447
+ module.exports = {
1448
+ PARCEL_MANIFEST_KIND,
1449
+ PARCEL_MANIFEST_SCHEMA_VERSION,
1450
+ SUPPORTED_PARCEL_MANIFEST_SCHEMA_VERSIONS,
1451
+ PARCEL_BLOCK_FIELDS,
1452
+ TRUST_NOTE,
1453
+ PARCEL_TRUST_NOTE,
1454
+ VERIFY_STATUS,
1455
+ normalizeParcelBlock,
1456
+ validateParcelBlock,
1457
+ buildParcelManifest,
1458
+ validateParcelManifest,
1459
+ readParcelManifest,
1460
+ writeParcelManifest,
1461
+ runParcelBuild,
1462
+ formatParcelVerify,
1463
+ runParcelVerify,
1464
+ // attest / verify-attest (T-18.3) — over the SAME signed-attestation core as `vh dataset`.
1465
+ PARCEL_ATTESTATION_KIND,
1466
+ PARCEL_ATTESTATION_SCHEMA_VERSION,
1467
+ SUPPORTED_PARCEL_ATTESTATION_SCHEMA_VERSIONS,
1468
+ PARCEL_ATTESTATION_TRUST_NOTE,
1469
+ SIGNED_PARCEL_ATTESTATION_KIND,
1470
+ SIGNED_PARCEL_ATTESTATION_SCHEMES,
1471
+ SIGNED_PARCEL_ATTESTATION_TRUST_NOTE,
1472
+ PARCEL_VERIFY_ATTEST_TRUST_NOTE,
1473
+ VERIFY_ATTEST_VERDICT,
1474
+ canonicalParcelFiles,
1475
+ parcelManifestDigest,
1476
+ buildParcelAttestation,
1477
+ validateParcelAttestation,
1478
+ serializeParcelAttestation,
1479
+ readParcelAttestation,
1480
+ validateSignedParcelAttestation,
1481
+ buildSignedParcelAttestation,
1482
+ serializeSignedParcelAttestation,
1483
+ readSignedParcelAttestation,
1484
+ recoverSignedParcelAttestationSigner,
1485
+ verifySignedParcelAttestation,
1486
+ formatParcelVerifyAttest,
1487
+ runParcelAttest,
1488
+ SIGN_TRUST_NOTE,
1489
+ runParcelSign,
1490
+ runParcelVerifyAttest,
1491
+ // timestamp (T-20.2) — detached RFC-3161 container over the SAME generic timestamp core.
1492
+ TIMESTAMPED_PARCEL_ATTESTATION_KIND,
1493
+ TIMESTAMPED_PARCEL_ATTESTATION_SCHEMA_VERSION,
1494
+ SUPPORTED_TIMESTAMPED_PARCEL_ATTESTATION_SCHEMA_VERSIONS,
1495
+ TIMESTAMPED_PARCEL_ATTESTATION_TRUST_NOTE,
1496
+ PARCEL_TIMESTAMP_REQUEST_TRUST_NOTE,
1497
+ validateTimestampedParcelAttestation,
1498
+ buildTimestampedParcelAttestation,
1499
+ serializeTimestampedParcelAttestation,
1500
+ readTimestampedParcelAttestation,
1501
+ runParcelTimestampRequest,
1502
+ runParcelTimestampWrap,
1503
+ // verify-timestamp (T-20.3) — OFFLINE independent-timestamp verifier over the SAME generic core.
1504
+ PARCEL_VERIFY_TIMESTAMP_VERDICT,
1505
+ PARCEL_VERIFY_TIMESTAMP_TRUST_NOTE,
1506
+ verifyTimestampedParcelAttestation,
1507
+ formatParcelVerifyTimestamp,
1508
+ runParcelVerifyTimestamp,
1509
+ };