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
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+
3
+ // cli/serve-verify-http.js — the loopback-only HTTP transport for `vh serve-verify` (T-59.2).
4
+ //
5
+ // WHAT THIS IS
6
+ // A tiny, dependency-free (Node-core `http` ONLY) HTTP server that fronts the PURE, transport-agnostic
7
+ // verify core (`cli/serve-verify.js › verifyRequest`). It transports a request body IN over HTTP, hands
8
+ // the already-parsed object to `verifyRequest`, and maps the returned verdict to a CI-mappable HTTP
9
+ // status + JSON body OUT. It invents NO verify logic, NO crypto, NO verdict vocabulary — every ACCEPT/
10
+ // REJECT/ERROR answer is the core's, byte-for-byte in `detail`.
11
+ //
12
+ // VERIFY-ONLY / NO KEY / NO FILE WRITES
13
+ // This server VERIFIES; it never SIGNS. It holds NO private key (it never constructs a Wallet, never
14
+ // reads a key, never calls signMessage/signSealWith). It writes NOTHING to disk — the whole request path
15
+ // (readBody -> JSON.parse -> verifyRequest -> sendJson) is I/O-free except the network socket. A verify
16
+ // therefore leaves the filesystem untouched, and the process holds no secret at any point.
17
+ //
18
+ // LOOPBACK BY DEFAULT (a human deploy step to expose)
19
+ // The default bind host is `127.0.0.1` (loopback). A request arriving on a non-loopback interface is NOT
20
+ // served by the default bind — the socket simply is not listening there. Exposing this publicly (behind
21
+ // YOUR nginx/Cloudflare, on YOUR domain, with TLS) is an explicit HUMAN deploy step; it is NEVER
22
+ // auto-deployed and NEVER binds a public interface on its own. (An operator MAY pass `--host 0.0.0.0` to
23
+ // bind all interfaces — that is their explicit, deliberate choice, printed back with a warning.)
24
+ //
25
+ // STATUS MAPPING (CI-mappable — a build can gate on the code alone)
26
+ // POST /verify:
27
+ // verdict ACCEPTED -> 200 (the seal/container verified)
28
+ // verdict REJECTED -> 422 (a well-formed request that did NOT verify — Unprocessable Entity)
29
+ // verdict ERROR -> 400 (a malformed/unknown request the core evaluated to ERROR — the body is bad)
30
+ // A wire body larger than --max-body is refused with 413 (Payload Too Large) BEFORE it is fully buffered —
31
+ // a DISTINCT code from the 400 bucket, so a CI gate can tell "too big" apart from "malformed/unknown".
32
+ // GET /healthz -> 200 { ok:true }
33
+ // Anything else (wrong method / wrong path) -> 404 (or 405 for a known path, wrong method).
34
+ //
35
+ // FAIL CLOSED
36
+ // Invalid JSON, an oversized body, an unknown route, or ANY unexpected internal error becomes a clean
37
+ // named JSON error with a 4xx/5xx status — NEVER a stack trace, NEVER a silent 200, NEVER a crash of the
38
+ // process. A caller that cannot be verified is never treated as verified.
39
+
40
+ const http = require("http");
41
+
42
+ const serveVerify = require("./serve-verify");
43
+
44
+ // The default bind host is LOOPBACK: a non-loopback interface is NOT served unless the operator explicitly
45
+ // asks for a different --host. The default port is arbitrary-but-memorable and does NOT collide with the
46
+ // TrustLedger browser door (4173).
47
+ const DEFAULT_HOST = "127.0.0.1";
48
+ const DEFAULT_PORT = 4180;
49
+
50
+ // The wire-level body cap enforced AS bytes arrive, so a hostile/accidental giant body is refused (413)
51
+ // before it is ever fully buffered. Defaults to the CORE's own byte cap so the two layers agree; an
52
+ // operator may lower it via --max-body. (The core ALSO re-checks the parsed size — belt and braces.)
53
+ const DEFAULT_MAX_BODY_BYTES = serveVerify.MAX_BODY_BYTES;
54
+
55
+ // The single verify route + the health route.
56
+ const VERIFY_PATH = "/verify";
57
+ const HEALTH_PATH = "/healthz";
58
+
59
+ // HTTP status codes we map onto. 422 (Unprocessable Entity) is the RIGHT code for "well-formed request the
60
+ // server understood, but the content did NOT verify" — distinct from 400 ("the request itself is bad").
61
+ const STATUS = Object.freeze({
62
+ OK: 200,
63
+ BAD_REQUEST: 400,
64
+ NOT_FOUND: 404,
65
+ METHOD_NOT_ALLOWED: 405,
66
+ UNPROCESSABLE: 422,
67
+ PAYLOAD_TOO_LARGE: 413,
68
+ INTERNAL: 500,
69
+ });
70
+
71
+ // The verbatim caveat block the CLI banner AND (a machine-readable copy) the /healthz body carry. These
72
+ // four lines are the acceptance-pinned posture: verify-only, loopback, P-3, human-deploy. Kept as an array
73
+ // of exact strings so a test can assert each line VERBATIM and the wording can never silently drift.
74
+ const CAVEATS = Object.freeze([
75
+ "verify-only: this server VERIFIES seals; it never signs, holds NO private key, and writes NO file.",
76
+ "loopback by default: it binds 127.0.0.1 — a non-loopback interface is NOT served unless you pass --host.",
77
+ "NOT a timestamp: a verified seal proves set-membership / a signer vouched, NOT \"sealed since date T\" (P-3).",
78
+ "exposing it publicly is a HUMAN deploy step (your nginx/Cloudflare, your domain, TLS) — never auto-deployed.",
79
+ ]);
80
+
81
+ // ---------------------------------------------------------------------------------------------------
82
+ // Body reader with a HARD size cap enforced as bytes arrive (never buffers past the cap).
83
+ // ---------------------------------------------------------------------------------------------------
84
+
85
+ class HttpError extends Error {
86
+ constructor(status, code, message) {
87
+ super(message);
88
+ this.name = "HttpError";
89
+ this.status = status;
90
+ this.code = code;
91
+ }
92
+ }
93
+
94
+ // A hard ceiling on how many bytes we will DRAIN past the cap before forcibly tearing the socket down. Once
95
+ // the body exceeds `maxBytes` we STOP buffering (memory stays bounded at the cap) but keep DRAINING incoming
96
+ // chunks so we can still send a clean 400 back to a merely-too-big (not hostile) client. If a truly hostile
97
+ // client keeps streaming far beyond the cap, we destroy the socket rather than drain forever.
98
+ const OVERSIZE_DRAIN_LIMIT = 4 * 1024 * 1024;
99
+
100
+ // Read the request body with a HARD byte cap enforced AS chunks arrive, so an oversized body never pins
101
+ // unbounded memory. Resolves to `{ body }` on a body within the cap, or `{ tooLarge:true }` once the cap is
102
+ // exceeded (the caller maps that to a clean 413 — never a silent ACCEPT). Rejects with an HttpError only on
103
+ // a transport error or a hostile client that keeps streaming far past the cap (socket torn down, also 413).
104
+ function readBody(req, maxBytes) {
105
+ return new Promise((resolve, reject) => {
106
+ const chunks = [];
107
+ let size = 0;
108
+ let over = false;
109
+ let done = false;
110
+ const finish = (fn, arg) => {
111
+ if (done) return;
112
+ done = true;
113
+ fn(arg);
114
+ };
115
+ req.on("data", (chunk) => {
116
+ size += chunk.length;
117
+ if (!over && size > maxBytes) {
118
+ // Cap exceeded: stop buffering (drop what we have so memory can't grow past the cap) and mark the
119
+ // request too-large. We keep draining to `end` so a clean 413 reaches a merely-oversized client.
120
+ over = true;
121
+ chunks.length = 0;
122
+ }
123
+ if (over) {
124
+ if (size > maxBytes + OVERSIZE_DRAIN_LIMIT) {
125
+ // A hostile client streaming far past the cap: tear the socket down rather than drain forever.
126
+ req.destroy();
127
+ finish(reject, new HttpError(STATUS.PAYLOAD_TOO_LARGE, "payload_too_large", `request body exceeds the ${maxBytes}-byte limit`));
128
+ }
129
+ return; // drop the chunk; do not buffer
130
+ }
131
+ chunks.push(chunk);
132
+ });
133
+ req.on("end", () =>
134
+ finish(resolve, over ? { tooLarge: true, maxBytes } : { body: Buffer.concat(chunks).toString("utf8") })
135
+ );
136
+ req.on("error", (e) => finish(reject, new HttpError(STATUS.BAD_REQUEST, "request_error", e.message)));
137
+ });
138
+ }
139
+
140
+ // Write a JSON response with a stable shape. The body is ALWAYS JSON (never an HTML page or a stack trace),
141
+ // so a programmatic CI client can always parse it. `nosniff` keeps it from being reinterpreted.
142
+ function sendJson(res, status, obj) {
143
+ const body = JSON.stringify(obj);
144
+ res.writeHead(status, {
145
+ "content-type": "application/json; charset=utf-8",
146
+ "content-length": Buffer.byteLength(body),
147
+ "x-content-type-options": "nosniff",
148
+ });
149
+ res.end(body);
150
+ }
151
+
152
+ // Send a NAMED JSON error — never a stack trace. An HttpError carries its own status + code; anything else
153
+ // is a defensive 500 with a generic message (its .message is NOT leaked, since it could be unexpected).
154
+ function sendError(res, err) {
155
+ if (err instanceof HttpError) {
156
+ sendJson(res, err.status, { error: err.code, message: err.message });
157
+ return;
158
+ }
159
+ sendJson(res, STATUS.INTERNAL, { error: "internal_error", message: "an internal error occurred" });
160
+ }
161
+
162
+ // Map the core verdict envelope to a CI-mappable HTTP status. ACCEPTED->200, REJECTED->422, ERROR->400.
163
+ // Anything unexpected (should be impossible — verifyRequest returns only those three) is a defensive 500.
164
+ function statusForVerdict(verdict) {
165
+ switch (verdict) {
166
+ case serveVerify.VERDICT.ACCEPTED:
167
+ return STATUS.OK;
168
+ case serveVerify.VERDICT.REJECTED:
169
+ return STATUS.UNPROCESSABLE;
170
+ case serveVerify.VERDICT.ERROR:
171
+ return STATUS.BAD_REQUEST;
172
+ default:
173
+ return STATUS.INTERNAL;
174
+ }
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------------------------------
178
+ // The request handler. Routes on method + path; delegates ALL verify logic to the pure core.
179
+ // ---------------------------------------------------------------------------------------------------
180
+
181
+ function makeHandler(opts = {}) {
182
+ const maxBytes =
183
+ opts.maxBodyBytes != null && Number.isFinite(opts.maxBodyBytes)
184
+ ? opts.maxBodyBytes
185
+ : DEFAULT_MAX_BODY_BYTES;
186
+
187
+ return function handler(req, res) {
188
+ // Route on the PATH only (ignore any query string).
189
+ const url = req.url || "/";
190
+ const pathOnly = url.split("?")[0];
191
+ const method = req.method || "GET";
192
+
193
+ // GET /healthz -> 200 { ok:true } (+ the machine-readable service metadata + caveats). A liveness/
194
+ // readiness probe (k8s, a CI healthcheck) hits this; it holds no key and touches nothing.
195
+ if (pathOnly === HEALTH_PATH) {
196
+ if (method !== "GET") {
197
+ sendError(
198
+ res,
199
+ new HttpError(STATUS.METHOD_NOT_ALLOWED, "method_not_allowed", `${HEALTH_PATH} accepts only GET`)
200
+ );
201
+ return;
202
+ }
203
+ sendJson(res, STATUS.OK, {
204
+ ok: true,
205
+ service: serveVerify.SERVICE_NAME,
206
+ schema: serveVerify.VERIFY_REQUEST_SCHEMA,
207
+ verifyOnly: true,
208
+ holdsKey: false,
209
+ writesFiles: false,
210
+ caveats: CAVEATS,
211
+ });
212
+ return;
213
+ }
214
+
215
+ // POST /verify -> run the pure core, map the verdict to a status.
216
+ if (pathOnly === VERIFY_PATH) {
217
+ if (method !== "POST") {
218
+ sendError(
219
+ res,
220
+ new HttpError(STATUS.METHOD_NOT_ALLOWED, "method_not_allowed", `${VERIFY_PATH} accepts only POST`)
221
+ );
222
+ return;
223
+ }
224
+ readBody(req, maxBytes)
225
+ .then((raw) => {
226
+ // An oversized body is a clean 413 (Payload Too Large) — a distinct, CI-gateable code, never a
227
+ // crash, never an ACCEPT. 413 (not 400) is the RIGHT status: the request was well-formed at the
228
+ // HTTP layer but its body exceeded the operator's --max-body cap.
229
+ if (raw.tooLarge) {
230
+ throw new HttpError(
231
+ STATUS.PAYLOAD_TOO_LARGE,
232
+ "payload_too_large",
233
+ `request body exceeds the ${raw.maxBytes}-byte limit`
234
+ );
235
+ }
236
+ let body;
237
+ try {
238
+ body = JSON.parse(raw.body);
239
+ } catch (e) {
240
+ // Malformed JSON is a clean 400 named error — never a crash, never a silent ACCEPT. (The core
241
+ // never sees it; a non-parseable body is a request-layer fault, mapped like an ERROR verdict.)
242
+ throw new HttpError(STATUS.BAD_REQUEST, "invalid_json", `request body is not valid JSON: ${e.message}`);
243
+ }
244
+ // The pure core does ALL verify work: it NEVER throws, holds no key, touches no fs/net. We only
245
+ // map its top-level verdict to a status and send its envelope back VERBATIM.
246
+ const verdict = serveVerify.verifyRequest(body);
247
+ sendJson(res, statusForVerdict(verdict.verdict), verdict);
248
+ })
249
+ .catch((err) => sendError(res, err));
250
+ return;
251
+ }
252
+
253
+ // Any other path/method: a named 404 (JSON, never an HTML error page).
254
+ sendError(res, new HttpError(STATUS.NOT_FOUND, "not_found", `no route for ${method} ${pathOnly}`));
255
+ };
256
+ }
257
+
258
+ // Build (but do NOT listen on) an http.Server. Keeping creation and listening separate lets a test bind an
259
+ // EPHEMERAL port (0) on 127.0.0.1 and close cleanly, and lets a real deploy choose its own port/host.
260
+ function createServer(opts = {}) {
261
+ return http.createServer(makeHandler(opts));
262
+ }
263
+
264
+ // The verbatim CLI banner printed when the server starts. LEADS with the verify-only + loopback + P-3 +
265
+ // human-deploy caveats (each line VERBATIM from CAVEATS) so an operator sees the posture before use.
266
+ function banner(url, host) {
267
+ const browseHint =
268
+ host === "0.0.0.0" || host === "::"
269
+ ? ` (${host} binds ALL interfaces — you chose to expose it; browse via your machine's own address.)\n`
270
+ : "";
271
+ return (
272
+ `vh serve-verify listening on ${url}\n` +
273
+ browseHint +
274
+ ` POST ${VERIFY_PATH} -> JSON verdict (200 ACCEPTED / 422 REJECTED / 400 bad request / 413 too large)\n` +
275
+ ` GET ${HEALTH_PATH} -> { ok:true }\n` +
276
+ CAVEATS.map((c) => ` - ${c}`).join("\n") +
277
+ "\n" +
278
+ " Press Ctrl-C to stop.\n"
279
+ );
280
+ }
281
+
282
+ module.exports = {
283
+ createServer,
284
+ makeHandler,
285
+ banner,
286
+ readBody,
287
+ sendJson,
288
+ sendError,
289
+ statusForVerdict,
290
+ HttpError,
291
+ CAVEATS,
292
+ DEFAULT_HOST,
293
+ DEFAULT_PORT,
294
+ DEFAULT_MAX_BODY_BYTES,
295
+ VERIFY_PATH,
296
+ HEALTH_PATH,
297
+ STATUS,
298
+ };
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+
3
+ // cli/serve-verify.js — the PURE, transport-agnostic verify CORE for the `vh serve-verify` service (T-59.1).
4
+ //
5
+ // WHAT THIS IS
6
+ // `verifyRequest(body)` takes a single, already-parsed request OBJECT (whatever a transport — an HTTP
7
+ // handler, a queue worker, a test — hands it) and returns a stable, versioned verdict OBJECT. It NEVER
8
+ // throws, NEVER touches the network, NEVER touches the filesystem, NEVER signs, and holds NO key. It is
9
+ // the composition layer that lets a CI pipeline / another microservice send "here is a seal (or a signed
10
+ // container), tell me ACCEPT/REJECT" over ANY transport, by dispatching on the request's `kind` to the
11
+ // already-green, already-tested SDK verify cores:
12
+ //
13
+ // kind "verify-seal" -> evidence.verifySeal(seal, entries) (UNSIGNED tamper-evidence)
14
+ // kind "verify-signed-seal" -> evidence.verifySignedSeal({container, ...}) (SIGNED / vendor-pinned)
15
+ //
16
+ // It re-uses the EXISTING verdict field names VERBATIM (it spreads the core verdict into `verdict.detail`
17
+ // unchanged) so a downstream consumer depends on ONE stable shape across the CLI, the SDK, and this
18
+ // service. It adds only a THIN, versioned request-level envelope (`schema`, `service`, `outcome`) around
19
+ // that core verdict — it invents NO new crypto, NO new verify logic, NO new verdict vocabulary.
20
+ //
21
+ // PURITY (asserted by the tests — a grep proves NONE of these appear in this module)
22
+ // NO `require("http")` / `require("https")` / `require("net")` / `require("fs")` / `require("dns")`.
23
+ // NO key material: it never calls `Wallet.createRandom`, never reads a private key, never signs. The
24
+ // ONLY dependency is `./evidence` (the SDK cores), and the ONLY core functions it calls are the PURE,
25
+ // filesystem-free verify/parse/build helpers (verifySeal / verifySignedSeal / readSeal /
26
+ // validateSignedSeal / buildSeal / serializeSeal) — none of which read a file, open a socket, or need a
27
+ // key. The function is therefore drivable with the filesystem unavailable/irrelevant.
28
+ //
29
+ // TRANSPORT-AGNOSTIC (why the ENVELOPE, not raw bytes)
30
+ // The transport (HTTP body, queue message, …) delivers TEXT/JSON, so `entries[].content` arrives as a
31
+ // STRING with an explicit `encoding` ("utf8" | "base64" | "hex"). This module decodes that back into the
32
+ // Buffer the byte-based cores require, RE-DERIVING the Merkle root from the caller-supplied bytes — never
33
+ // from the seal's own stored hashes. So a one-byte tamper in `content` still flips ACCEPTED -> REJECTED,
34
+ // exactly as the CLI does.
35
+ //
36
+ // FAIL CLOSED (never a silent/false ACCEPT)
37
+ // Every malformed body, oversized body, unknown `kind`, undecodable entry, or unexpected internal error
38
+ // maps to `verdict:"ERROR"` with a machine-readable `code` and a human `message` — NEVER `"ACCEPTED"`,
39
+ // NEVER a thrown exception that could crash a server or be swallowed into a 200. An ERROR is the safe
40
+ // default: a caller that cannot be verified must not be treated as verified.
41
+
42
+ const evidence = require("./evidence");
43
+
44
+ // The versioned envelope contract. Bump SCHEMA only on a breaking change to the ENVELOPE fields (never for
45
+ // a change inside `detail`, which tracks the core verify verdicts' own shape).
46
+ const VERIFY_REQUEST_SCHEMA = "vh.verify-request/1";
47
+ const SERVICE_NAME = "vh-serve-verify";
48
+
49
+ // The request `kind`s this core dispatches on (the REQUEST kind — distinct from the seal/container `kind`
50
+ // values `evidence.SEAL_KIND` / `evidence.SIGNED_SEAL_KIND` those payloads carry INSIDE the request).
51
+ const KIND_VERIFY_SEAL = "verify-seal";
52
+ const KIND_VERIFY_SIGNED_SEAL = "verify-signed-seal";
53
+ const SUPPORTED_KINDS = Object.freeze([KIND_VERIFY_SEAL, KIND_VERIFY_SIGNED_SEAL]);
54
+
55
+ // The three top-level verdicts. ERROR is NOT a content verdict — it means "the request could not be
56
+ // evaluated", strictly separate from a content REJECTED (which IS an evaluated, negative answer).
57
+ const VERDICT = Object.freeze({ ACCEPTED: "ACCEPTED", REJECTED: "REJECTED", ERROR: "ERROR" });
58
+
59
+ // Machine-readable ERROR codes (stable; a caller can branch on these without parsing prose).
60
+ const ERR = Object.freeze({
61
+ NOT_OBJECT: "ERR_BODY_NOT_OBJECT",
62
+ TOO_LARGE: "ERR_BODY_TOO_LARGE",
63
+ UNKNOWN_KIND: "ERR_UNKNOWN_KIND",
64
+ MISSING_SEAL: "ERR_MISSING_SEAL",
65
+ BAD_SEAL: "ERR_BAD_SEAL",
66
+ BAD_ENTRIES: "ERR_BAD_ENTRIES",
67
+ MISSING_CONTAINER: "ERR_MISSING_CONTAINER",
68
+ BAD_CONTAINER: "ERR_BAD_CONTAINER",
69
+ BAD_EXPECTED_SIGNER: "ERR_BAD_EXPECTED_SIGNER",
70
+ INTERNAL: "ERR_INTERNAL",
71
+ });
72
+
73
+ // A generous upper bound on the request size (bytes of the estimated payload), so a hostile/accidental
74
+ // giant body is a clean ERROR rather than an OOM. The transport can (and should) also cap the wire size;
75
+ // this is the core's own belt-and-braces. 8 MiB comfortably fits a large seal + its inlined content while
76
+ // staying far below anything that would strain the process.
77
+ const MAX_BODY_BYTES = 8 * 1024 * 1024;
78
+
79
+ const SUPPORTED_ENTRY_ENCODINGS = Object.freeze(["utf8", "base64", "hex"]);
80
+
81
+ /**
82
+ * The stable, versioned ERROR verdict envelope. Never contains an ACCEPTED/REJECTED content answer — an
83
+ * ERROR means the request itself could not be evaluated. Pure; allocates a plain object only.
84
+ * @param {string} code one of ERR.*
85
+ * @param {string} message a human-readable, non-sensitive reason
86
+ * @returns {object}
87
+ */
88
+ function errorVerdict(code, message) {
89
+ return {
90
+ schema: VERIFY_REQUEST_SCHEMA,
91
+ service: SERVICE_NAME,
92
+ verdict: VERDICT.ERROR,
93
+ code: String(code),
94
+ message: String(message),
95
+ };
96
+ }
97
+
98
+ /**
99
+ * The stable, versioned OK envelope wrapping a core verify verdict VERBATIM. `outcome` mirrors the core's
100
+ * own `verdict` ("ACCEPTED"|"REJECTED") so a transport can map it to a status code without reading `detail`;
101
+ * `detail` is the UNCHANGED core verdict object (its field names are the contract the CLI/SDK already ship).
102
+ * @param {string} kind the request kind that was dispatched
103
+ * @param {object} detail the core verdict from verifySeal / verifySignedSeal (spread unchanged)
104
+ * @returns {object}
105
+ */
106
+ function okVerdict(kind, detail) {
107
+ // `detail.verdict` is the core's own field ("ACCEPTED"|"REJECTED"). We surface it at the top level as the
108
+ // envelope's `verdict` too, so the top-level answer and the detail's answer are ALWAYS the same string —
109
+ // a transport reads the top level; an auditor reads `detail`. We NEVER recompute or override it.
110
+ const outcome = detail.verdict;
111
+ return {
112
+ schema: VERIFY_REQUEST_SCHEMA,
113
+ service: SERVICE_NAME,
114
+ verdict: outcome, // ACCEPTED | REJECTED — copied from the core verdict, never re-derived
115
+ kind,
116
+ detail, // the EXISTING core verdict shape, byte-for-byte (field names unchanged)
117
+ };
118
+ }
119
+
120
+ // Rough byte-size of an already-parsed body without re-serializing giant nested structures repeatedly. We
121
+ // only need an UPPER-bound guard, so a single JSON.stringify is acceptable and stays pure (string in memory,
122
+ // no I/O). Guarded in a try/catch: a body with a circular ref or a BigInt can't be stringified — that is
123
+ // itself a malformed request, reported as an oversized/invalid error rather than a throw.
124
+ function estimateBodyBytes(body) {
125
+ const json = JSON.stringify(body);
126
+ // JSON.stringify returns undefined for a value that serializes to nothing (e.g. a function); treat that as
127
+ // 0 here — the shape checks below will reject a non-object body anyway.
128
+ return json === undefined ? 0 : Buffer.byteLength(json, "utf8");
129
+ }
130
+
131
+ /**
132
+ * Decode one transport entry `{ relPath, content, encoding }` into the `{ relPath, bytes: Buffer }` shape
133
+ * the byte-based seal core consumes. PURE: string -> Buffer, no I/O. Throws a plain Error (caught by the
134
+ * caller and mapped to a clean ERROR verdict) on any malformed entry — NEVER partially accepts.
135
+ */
136
+ function decodeEntry(entry, index) {
137
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
138
+ throw new Error(`entries[${index}] must be an object { relPath, content, encoding }`);
139
+ }
140
+ const { relPath, content } = entry;
141
+ if (typeof relPath !== "string" || relPath.length === 0) {
142
+ throw new Error(`entries[${index}].relPath must be a non-empty string`);
143
+ }
144
+ if (typeof content !== "string") {
145
+ throw new Error(`entries[${index}].content must be a string (the ${index}th entry's bytes, encoded)`);
146
+ }
147
+ const encoding = entry.encoding === undefined ? "utf8" : entry.encoding;
148
+ if (!SUPPORTED_ENTRY_ENCODINGS.includes(encoding)) {
149
+ throw new Error(
150
+ `entries[${index}].encoding must be one of ${SUPPORTED_ENTRY_ENCODINGS.join("|")} (got ${JSON.stringify(encoding)})`
151
+ );
152
+ }
153
+ // For base64/hex, Buffer.from is lenient (it drops invalid chars) — so we round-trip and require the
154
+ // re-encoding to match, catching a corrupt/garbage payload instead of silently hashing truncated bytes.
155
+ const bytes = Buffer.from(content, encoding);
156
+ if (encoding === "base64" || encoding === "hex") {
157
+ const reEncoded = bytes.toString(encoding);
158
+ const normalizedInput = encoding === "hex" ? content.toLowerCase() : content;
159
+ const normalizedOut = encoding === "hex" ? reEncoded.toLowerCase() : reEncoded;
160
+ if (normalizedOut !== normalizedInput) {
161
+ throw new Error(`entries[${index}].content is not valid ${encoding}`);
162
+ }
163
+ }
164
+ return { relPath, bytes };
165
+ }
166
+
167
+ /** Decode a transport `entries[]` array into the byte-based entry list. Throws on any malformed member. */
168
+ function decodeEntries(rawEntries) {
169
+ if (!Array.isArray(rawEntries)) {
170
+ throw new Error("`entries` must be an array of { relPath, content, encoding }");
171
+ }
172
+ return rawEntries.map((e, i) => decodeEntry(e, i));
173
+ }
174
+
175
+ /**
176
+ * verifyRequest(body) — the PURE, transport-agnostic verify dispatcher.
177
+ *
178
+ * @param {object} body an already-parsed request object:
179
+ * UNSIGNED: { kind: "verify-seal", seal: <object|string>, entries: [{relPath, content, encoding}] }
180
+ * SIGNED: { kind: "verify-signed-seal", container: <object|string>,
181
+ * expectedSigner?: <0x-address>, entries?: [{relPath, content, encoding}] }
182
+ * (when `entries` is supplied on the SIGNED path, it is BOUND: the canonical seal bytes are
183
+ * recomputed from those entries and required byte-identical to the signed payload; a set that
184
+ * does NOT match the signed bytes is a clean REJECTED, never an ACCEPT.)
185
+ * @returns {object} a stable versioned envelope:
186
+ * OK: { schema, service, verdict:"ACCEPTED"|"REJECTED", kind, detail:<core verdict, fields unchanged> }
187
+ * ERROR: { schema, service, verdict:"ERROR", code, message }
188
+ *
189
+ * NEVER throws. NEVER returns a false ACCEPT for a malformed/oversized/unknown request.
190
+ */
191
+ function verifyRequest(body) {
192
+ try {
193
+ // --- Envelope-level guards (fail closed) --------------------------------------------------------
194
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
195
+ return errorVerdict(ERR.NOT_OBJECT, "request body must be a JSON object");
196
+ }
197
+ let size;
198
+ try {
199
+ size = estimateBodyBytes(body);
200
+ } catch (_) {
201
+ // Un-stringifiable (circular ref / BigInt / etc.) — a malformed request, reported as too-large-ish.
202
+ return errorVerdict(ERR.TOO_LARGE, "request body could not be measured (not plain JSON)");
203
+ }
204
+ if (size > MAX_BODY_BYTES) {
205
+ return errorVerdict(
206
+ ERR.TOO_LARGE,
207
+ `request body is ${size} bytes; the limit is ${MAX_BODY_BYTES} bytes`
208
+ );
209
+ }
210
+
211
+ const kind = body.kind;
212
+ if (kind === KIND_VERIFY_SEAL) return verifySealRequest(body);
213
+ if (kind === KIND_VERIFY_SIGNED_SEAL) return verifySignedSealRequest(body);
214
+ return errorVerdict(
215
+ ERR.UNKNOWN_KIND,
216
+ `unknown request kind ${JSON.stringify(kind)}; expected one of ${SUPPORTED_KINDS.map((k) => JSON.stringify(k)).join(", ")}`
217
+ );
218
+ } catch (e) {
219
+ // Defense-in-depth: anything unexpected inside a handler becomes a clean ERROR, never a crash or a
220
+ // silent ACCEPT. The message is the error's own text (non-sensitive — no key/file/network here).
221
+ return errorVerdict(ERR.INTERNAL, e && e.message ? String(e.message) : "internal error");
222
+ }
223
+ }
224
+
225
+ /** Handle a { kind:"verify-seal", seal, entries } request. Pure — decodes entries, calls verifySeal. */
226
+ function verifySealRequest(body) {
227
+ if (body.seal === undefined || body.seal === null) {
228
+ return errorVerdict(ERR.MISSING_SEAL, "`seal` is required for kind \"verify-seal\"");
229
+ }
230
+ // Strictly parse+validate the seal FIRST (readSeal accepts a JSON string OR an object; it rejects a
231
+ // corrupt/foreign/wrong-kind seal before any byte work — never half-accepted).
232
+ let seal;
233
+ try {
234
+ seal = evidence.readSeal(body.seal);
235
+ } catch (e) {
236
+ return errorVerdict(ERR.BAD_SEAL, `invalid seal: ${e.message}`);
237
+ }
238
+ let entries;
239
+ try {
240
+ entries = decodeEntries(body.entries === undefined ? [] : body.entries);
241
+ } catch (e) {
242
+ return errorVerdict(ERR.BAD_ENTRIES, e.message);
243
+ }
244
+ // The authoritative, PURE verify — RE-DERIVES the root from the supplied bytes (a one-byte tamper flips it).
245
+ const detail = evidence.verifySeal(seal, entries);
246
+ return okVerdict(KIND_VERIFY_SEAL, detail);
247
+ }
248
+
249
+ /**
250
+ * Handle a { kind:"verify-signed-seal", container, expectedSigner?, entries? } request. Pure — validates
251
+ * the container, optionally recomputes the bound canonical bytes from `entries`, calls verifySignedSeal.
252
+ */
253
+ function verifySignedSealRequest(body) {
254
+ if (body.container === undefined || body.container === null) {
255
+ return errorVerdict(ERR.MISSING_CONTAINER, "`container` is required for kind \"verify-signed-seal\"");
256
+ }
257
+ // Parse the container if it arrived as a JSON string; then STRICT-validate it (rejects a tampered/foreign
258
+ // container — but NOT a forged signature, which the verify core catches by recovering the signer).
259
+ let container = body.container;
260
+ if (typeof container === "string") {
261
+ try {
262
+ container = JSON.parse(container);
263
+ } catch (e) {
264
+ return errorVerdict(ERR.BAD_CONTAINER, `container is not valid JSON: ${e.message}`);
265
+ }
266
+ }
267
+ if (container === null || typeof container !== "object" || Array.isArray(container)) {
268
+ return errorVerdict(ERR.BAD_CONTAINER, "`container` must be a signed-seal object or its JSON string");
269
+ }
270
+ try {
271
+ evidence.validateSignedSeal(container);
272
+ } catch (e) {
273
+ return errorVerdict(ERR.BAD_CONTAINER, `invalid signed container: ${e.message}`);
274
+ }
275
+
276
+ // OPTIONAL pin: an expected signer address the recovered signer must equal. We pass it straight to the
277
+ // core (which normalizes/validates the address); a malformed address is a clean ERROR here rather than a
278
+ // throw from deep in the core.
279
+ let expectedSigner;
280
+ if (body.expectedSigner !== undefined && body.expectedSigner !== null) {
281
+ if (typeof body.expectedSigner !== "string") {
282
+ return errorVerdict(ERR.BAD_EXPECTED_SIGNER, "`expectedSigner` must be a 0x-address string");
283
+ }
284
+ expectedSigner = body.expectedSigner;
285
+ }
286
+
287
+ // OPTIONAL binding: when `entries` is supplied, recompute the canonical UNSIGNED seal bytes from THOSE
288
+ // entries (the SAME serializeSeal(buildSeal(...)) the seal path embeds) and require them byte-identical to
289
+ // the signed payload. So an `entries` set that does NOT match the signed bytes is a clean REJECTED — never
290
+ // an ACCEPT. Absent `entries` = binding NOT requested (Check runs signature+optional-pin only).
291
+ let expectedCanonical;
292
+ if (body.entries !== undefined && body.entries !== null) {
293
+ let entries;
294
+ try {
295
+ entries = decodeEntries(body.entries);
296
+ } catch (e) {
297
+ return errorVerdict(ERR.BAD_ENTRIES, e.message);
298
+ }
299
+ try {
300
+ expectedCanonical = evidence.serializeSeal(evidence.buildSeal(entries));
301
+ } catch (e) {
302
+ // A set that can't even be sealed (e.g. duplicate relPaths) is a bad-entries request, not an ACCEPT.
303
+ return errorVerdict(ERR.BAD_ENTRIES, `could not seal supplied entries: ${e.message}`);
304
+ }
305
+ }
306
+
307
+ let detail;
308
+ try {
309
+ detail = evidence.verifySignedSeal({ container, expectedSigner, expectedCanonical });
310
+ } catch (e) {
311
+ // The core throws only on an unknown signature scheme (validateSignedSeal already rejects one, so this
312
+ // is defense-in-depth) or a malformed expectedSigner address. Either is a request error, never an ACCEPT.
313
+ return errorVerdict(ERR.BAD_EXPECTED_SIGNER, e.message);
314
+ }
315
+ return okVerdict(KIND_VERIFY_SIGNED_SEAL, detail);
316
+ }
317
+
318
+ module.exports = {
319
+ verifyRequest,
320
+ // Constants a transport / test can depend on without hard-coding strings.
321
+ VERIFY_REQUEST_SCHEMA,
322
+ SERVICE_NAME,
323
+ KIND_VERIFY_SEAL,
324
+ KIND_VERIFY_SIGNED_SEAL,
325
+ SUPPORTED_KINDS,
326
+ VERDICT,
327
+ ERR,
328
+ MAX_BODY_BYTES,
329
+ SUPPORTED_ENTRY_ENCODINGS,
330
+ // Exposed for tests / advanced transports that pre-decode; NOT part of the request envelope contract.
331
+ decodeEntry,
332
+ decodeEntries,
333
+ };