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,391 @@
1
+ "use strict";
2
+
3
+ // TrustLedger — server.js
4
+ //
5
+ // T-27.1: a MINIMAL, dependency-free web front-door over the EXISTING engine.
6
+ //
7
+ // EPIC-27's whole point: a property-management broker will never use a terminal,
8
+ // so the (complete, robust) CLI reconciliation engine is un-sellable as-is. This
9
+ // is the thin web door that turns "a tool I run in a terminal" into "a product a
10
+ // broker opens in a browser, drags three files into, and watches the balances tie
11
+ // out." It REUSES the engine VERBATIM — ingest -> match -> reconcile -> report —
12
+ // and adds NOTHING to the pipeline; it only transports bytes in over HTTP and the
13
+ // already-computed model + rendered packet back out as JSON.
14
+ //
15
+ // Pure Node `http` — NO new dependency. The browser reads the three files the
16
+ // broker drops and POSTs their TEXT CONTENTS as a JSON body, so there is NO
17
+ // multipart parsing here (that complexity lives in the browser's FileReader, not
18
+ // in a hand-rolled multipart parser on the server).
19
+ //
20
+ // Two routes:
21
+ // GET / -> the static single-page upload UI (no framework, no CDN).
22
+ // POST /api/reconcile -> { bank, ledger, rentroll, state?, priorClose? } (file
23
+ // CONTENTS as text) -> runs the pipeline and returns
24
+ // { tiesOut, balances, exceptions, reportHtml, reportCsv }.
25
+ //
26
+ // STRICT + SAFE, matching the engine's posture:
27
+ // * A malformed / ambiguous file raises a NAMED JSON error with HTTP 400 — never
28
+ // a stack trace, never a silent coercion. The named error is the SAME engine
29
+ // error (IngestError / ReportError / PolicyError / CloseError) so the broker
30
+ // sees the exact located reason ("malformed amount ... (row 3, bank)").
31
+ // * An oversized body is rejected with HTTP 413 (a named "payload_too_large")
32
+ // before it is ever buffered fully into memory, so a hostile client cannot
33
+ // exhaust the process.
34
+ // * The server NEVER writes to cwd (or anywhere): the entire pipeline through
35
+ // report.renderHTML / renderExceptionsCSV is PURE and I/O-free. No packet
36
+ // file, no temp file, no receipt is ever written. It is safe to run anywhere.
37
+ //
38
+ // HONEST POSTURE inherited verbatim: the returned reportHtml carries the SAME
39
+ // custodian disclaimer the CLI packet does (it is the identical renderHTML output).
40
+ // The web door changes HOW the broker reaches the engine, not WHAT it claims — it
41
+ // AIDS reconciliation; the broker remains the responsible trust-account custodian.
42
+ //
43
+ // T-65.2: the PURE payload→result core (HttpError, the T-29.3 license gate, and
44
+ // the reconcilePayload / inspectPayload handlers) now lives in ./door-core so the
45
+ // OFFLINE single-file app (trustledger/build-standalone.js) can inline the SAME
46
+ // functions verbatim. This file keeps ONLY the HTTP transport (routes, body cap,
47
+ // JSON envelope, static page) and RE-EXPORTS the core under the same names, so
48
+ // the door's observable behavior — statuses, error names, bytes — is unchanged
49
+ // and the two surfaces can never drift.
50
+
51
+ const http = require("http");
52
+ const fs = require("fs");
53
+ const path = require("path");
54
+
55
+ const doorCore = require("./door-core");
56
+ const {
57
+ HttpError,
58
+ WEB_PAID_FEATURE_ENTITLEMENTS,
59
+ gatePayload,
60
+ reconcilePayload,
61
+ inspectPayload,
62
+ } = doorCore;
63
+
64
+ // Hard cap on the POST body. Three monthly exports (bank CSV/OFX, QuickBooks CSV,
65
+ // rent roll CSV) are tiny — kilobytes to low single-digit megabytes. 16 MiB is a
66
+ // generous ceiling that still firmly bounds the memory a single request can pin,
67
+ // so a hostile or buggy client cannot stream an unbounded body into the process.
68
+ const MAX_BODY_BYTES = 16 * 1024 * 1024;
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // The payload→result core — HttpError, the T-29.3 license gate (gatePayload +
72
+ // WEB_PAID_FEATURE_ENTITLEMENTS), reconcilePayload, and the T-28.1 inspectPayload
73
+ // (which reuses ingest.diagnoseSource VERBATIM) — lives in ./door-core (T-65.2)
74
+ // and is re-exported below unchanged. See that file for the full contracts; this
75
+ // file is ONLY the HTTP transport around it.
76
+ // ---------------------------------------------------------------------------
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // HTTP plumbing
80
+ // ---------------------------------------------------------------------------
81
+
82
+ // Read the request body with a HARD size cap enforced AS bytes arrive, so an
83
+ // oversized body is rejected (HttpError 413) before it is fully buffered. Resolves
84
+ // to the body string; rejects with an HttpError on overflow.
85
+ function readBody(req) {
86
+ return new Promise((resolve, reject) => {
87
+ const chunks = [];
88
+ let size = 0;
89
+ let done = false;
90
+ const finish = (fn, arg) => {
91
+ if (done) return;
92
+ done = true;
93
+ fn(arg);
94
+ };
95
+ req.on("data", (chunk) => {
96
+ size += chunk.length;
97
+ if (size > MAX_BODY_BYTES) {
98
+ // Stop accepting more bytes and reject; do not buffer the rest.
99
+ req.destroy();
100
+ finish(
101
+ reject,
102
+ new HttpError(
103
+ 413,
104
+ "payload_too_large",
105
+ `request body exceeds the ${MAX_BODY_BYTES}-byte limit`
106
+ )
107
+ );
108
+ return;
109
+ }
110
+ chunks.push(chunk);
111
+ });
112
+ req.on("end", () => finish(resolve, Buffer.concat(chunks).toString("utf8")));
113
+ req.on("error", (e) =>
114
+ finish(reject, new HttpError(400, "request_error", e.message))
115
+ );
116
+ });
117
+ }
118
+
119
+ // Write a JSON response with a stable shape. The body is always JSON (never an
120
+ // HTML error page or a stack trace), so a programmatic client can always parse it.
121
+ function sendJson(res, status, obj) {
122
+ const body = JSON.stringify(obj);
123
+ res.writeHead(status, {
124
+ "content-type": "application/json; charset=utf-8",
125
+ "content-length": Buffer.byteLength(body),
126
+ // The browser page is served same-origin, so no CORS is needed; we set
127
+ // nosniff to keep the JSON from being interpreted as anything else.
128
+ "x-content-type-options": "nosniff",
129
+ });
130
+ res.end(body);
131
+ }
132
+
133
+ function sendError(res, err) {
134
+ const status = err instanceof HttpError ? err.status : 500;
135
+ const code = err instanceof HttpError ? err.code : "internal_error";
136
+ // NEVER leak a stack trace: send only the named code + the human message.
137
+ const message =
138
+ err instanceof HttpError ? err.message : "an internal error occurred";
139
+ sendJson(res, status, { error: code, message });
140
+ }
141
+
142
+ // The canonical single-page upload UI lives in trustledger/public/index.html —
143
+ // ONE self-contained file (no framework, no CDN) a designer can edit without
144
+ // touching server code. The server serves THAT file verbatim; this embedded copy
145
+ // is a byte-faithful FALLBACK so the door still works if the file is ever missing
146
+ // (e.g. a partial deploy). Both read the three dropped files with the browser's
147
+ // FileReader, POST their text to /api/reconcile, and render the returned verdict,
148
+ // balances, exception table, and download links. Read once + cached: the file is
149
+ // immutable at deploy time, so this stays I/O-cheap and deterministic per process.
150
+ const PUBLIC_INDEX = path.join(__dirname, "public", "index.html");
151
+ let cachedIndexHtml = null;
152
+
153
+ function indexHtml() {
154
+ if (cachedIndexHtml != null) return cachedIndexHtml;
155
+ try {
156
+ cachedIndexHtml = fs.readFileSync(PUBLIC_INDEX, "utf8");
157
+ } catch (_) {
158
+ cachedIndexHtml = embeddedIndexHtml();
159
+ }
160
+ return cachedIndexHtml;
161
+ }
162
+
163
+ // The byte-faithful fallback page (used only when public/index.html is absent).
164
+ function embeddedIndexHtml() {
165
+ return `<!doctype html>
166
+ <html lang="en">
167
+ <head>
168
+ <meta charset="utf-8">
169
+ <meta name="viewport" content="width=device-width, initial-scale=1">
170
+ <title>TrustLedger — Three-Way Trust-Account Reconciliation</title>
171
+ <style>
172
+ body { font: 15px/1.5 -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
173
+ color: #1a1a1a; max-width: 820px; margin: 2rem auto; padding: 0 1rem; }
174
+ h1 { font-size: 1.4rem; }
175
+ .note { color: #555; font-size: .9rem; }
176
+ fieldset { border: 1px solid #ddd; border-radius: 6px; margin: 1rem 0; padding: 1rem; }
177
+ legend { font-weight: 600; padding: 0 .4rem; }
178
+ label { display: block; margin: .6rem 0 .2rem; font-weight: 600; }
179
+ button { font: inherit; padding: .5rem 1rem; border-radius: 6px; border: 1px solid #0a6b2f;
180
+ background: #0a6b2f; color: #fff; cursor: pointer; }
181
+ button:disabled { opacity: .5; cursor: default; }
182
+ .verdict { display: inline-block; padding: .4rem .8rem; border-radius: 6px; font-weight: 700; margin: 1rem 0; }
183
+ .verdict.pass { background: #e6f4ea; color: #0a6b2f; border: 1px solid #0a6b2f; }
184
+ .verdict.fail { background: #fdeaea; color: #b00020; border: 1px solid #b00020; }
185
+ .err { color: #b00020; font-weight: 600; }
186
+ iframe { width: 100%; height: 560px; border: 1px solid #ddd; border-radius: 6px; margin-top: 1rem; }
187
+ .disclaimer { background: #fffbe6; border: 1px solid #e6d77a; border-radius: 6px;
188
+ padding: .6rem .9rem; font-size: .85rem; margin: 1rem 0; }
189
+ </style>
190
+ </head>
191
+ <body>
192
+ <h1>TrustLedger — Three-Way Trust-Account Reconciliation</h1>
193
+ <p class="note">Drop your three monthly files. Your browser reads them and sends
194
+ their contents to this server; the reconciliation runs in memory and nothing is
195
+ stored on disk.</p>
196
+ <div class="disclaimer"><strong>Disclaimer.</strong> This tool AIDS reconciliation.
197
+ The broker remains the legal trust-account custodian and is solely responsible for
198
+ the accuracy of the trust-account records. It is tamper-evidence and a reconciliation
199
+ aid only — NOT a trusted timestamp, NOT legal, accounting, or audit advice, and NOT a
200
+ substitute for a CPA's review.</div>
201
+
202
+ <form id="f">
203
+ <fieldset>
204
+ <legend>The three files</legend>
205
+ <label for="bank">Bank statement (CSV or OFX/QFX)</label>
206
+ <input id="bank" type="file" accept=".csv,.ofx,.qfx,.txt" required>
207
+ <label for="ledger">QuickBooks trust ledger (CSV)</label>
208
+ <input id="ledger" type="file" accept=".csv,.txt" required>
209
+ <label for="rentroll">Rent roll / tenant sub-ledger (CSV)</label>
210
+ <input id="rentroll" type="file" accept=".csv,.txt" required>
211
+ </fieldset>
212
+ <button id="go" type="submit">Reconcile</button>
213
+ </form>
214
+
215
+ <div id="result"></div>
216
+
217
+ <script>
218
+ (function () {
219
+ var form = document.getElementById("f");
220
+ var result = document.getElementById("result");
221
+
222
+ function read(input) {
223
+ return new Promise(function (resolve, reject) {
224
+ var file = input.files && input.files[0];
225
+ if (!file) { reject(new Error("please choose the " + input.id + " file")); return; }
226
+ var r = new FileReader();
227
+ r.onload = function () { resolve(String(r.result)); };
228
+ r.onerror = function () { reject(new Error("could not read " + input.id)); };
229
+ r.readAsText(file);
230
+ });
231
+ }
232
+
233
+ form.addEventListener("submit", function (ev) {
234
+ ev.preventDefault();
235
+ var go = document.getElementById("go");
236
+ go.disabled = true;
237
+ result.innerHTML = "<p>Reconciling…</p>";
238
+
239
+ Promise.all([
240
+ read(document.getElementById("bank")),
241
+ read(document.getElementById("ledger")),
242
+ read(document.getElementById("rentroll"))
243
+ ]).then(function (texts) {
244
+ return fetch("/api/reconcile", {
245
+ method: "POST",
246
+ headers: { "content-type": "application/json" },
247
+ body: JSON.stringify({ bank: texts[0], ledger: texts[1], rentroll: texts[2] })
248
+ });
249
+ }).then(function (resp) {
250
+ return resp.json().then(function (data) { return { ok: resp.ok, data: data }; });
251
+ }).then(function (r) {
252
+ go.disabled = false;
253
+ if (!r.ok) {
254
+ result.innerHTML = "<p class='err'>Error (" +
255
+ escapeHtml(r.data.error || "error") + "): " +
256
+ escapeHtml(r.data.message || "") + "</p>";
257
+ return;
258
+ }
259
+ var d = r.data;
260
+ var cls = d.tiesOut ? "pass" : "fail";
261
+ var verdict = d.pass ? "PASS — three-way reconciliation ties out"
262
+ : "FAIL — see exceptions";
263
+ var frame = document.createElement("iframe");
264
+ result.innerHTML = "<div class='verdict " + cls + "'>" + escapeHtml(verdict) + "</div>" +
265
+ "<p>" + escapeHtml(d.summary || "") + "</p>";
266
+ result.appendChild(frame);
267
+ frame.srcdoc = d.reportHtml;
268
+ }).catch(function (e) {
269
+ go.disabled = false;
270
+ result.innerHTML = "<p class='err'>" + escapeHtml(e.message || String(e)) + "</p>";
271
+ });
272
+ });
273
+
274
+ function escapeHtml(s) {
275
+ return String(s).replace(/[&<>"']/g, function (c) {
276
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
277
+ });
278
+ }
279
+ })();
280
+ </script>
281
+ </body>
282
+ </html>
283
+ `;
284
+ }
285
+
286
+ // The request handler. Pure of any module-level state except `today` (injectable).
287
+ function makeHandler(opts = {}) {
288
+ const today = opts.today || todayISO;
289
+ return function handler(req, res) {
290
+ // Parse the URL path only (ignore query); route on method + path.
291
+ const url = req.url || "/";
292
+ const pathOnly = url.split("?")[0];
293
+
294
+ if (req.method === "GET" && (pathOnly === "/" || pathOnly === "/index.html")) {
295
+ const body = indexHtml();
296
+ res.writeHead(200, {
297
+ "content-type": "text/html; charset=utf-8",
298
+ "content-length": Buffer.byteLength(body),
299
+ "x-content-type-options": "nosniff",
300
+ });
301
+ res.end(body);
302
+ return;
303
+ }
304
+
305
+ if (req.method === "POST" && pathOnly === "/api/reconcile") {
306
+ readBody(req)
307
+ .then((raw) => {
308
+ let payload;
309
+ try {
310
+ payload = JSON.parse(raw);
311
+ } catch (e) {
312
+ throw new HttpError(400, "invalid_json", `request body is not valid JSON: ${e.message}`);
313
+ }
314
+ const out = reconcilePayload(payload, today());
315
+ sendJson(res, 200, out);
316
+ })
317
+ .catch((err) => sendError(res, err));
318
+ return;
319
+ }
320
+
321
+ // T-28.1: the read-only per-file diagnostic. Additive + SEPARATE from
322
+ // /api/reconcile: it parses WITHOUT failing closed and reports every failing
323
+ // row (exactly as `vh trust inspect`), so a well-formed-but-unmatched file is
324
+ // a 200 with `requiredMissing`, not an error. Same body transport + named
325
+ // 400/413 posture; same no-cwd-write guarantee (diagnose is pure).
326
+ if (req.method === "POST" && pathOnly === "/api/inspect") {
327
+ readBody(req)
328
+ .then((raw) => {
329
+ let payload;
330
+ try {
331
+ payload = JSON.parse(raw);
332
+ } catch (e) {
333
+ throw new HttpError(400, "invalid_json", `request body is not valid JSON: ${e.message}`);
334
+ }
335
+ const out = inspectPayload(payload);
336
+ sendJson(res, 200, out);
337
+ })
338
+ .catch((err) => sendError(res, err));
339
+ return;
340
+ }
341
+
342
+ // Anything else: a named 404 (still JSON, never an HTML error page).
343
+ sendError(res, new HttpError(404, "not_found", `no route for ${req.method} ${pathOnly}`));
344
+ };
345
+ }
346
+
347
+ // Build (but do NOT listen on) an http.Server. The caller calls .listen(port).
348
+ // Keeping creation and listening separate lets tests bind an EPHEMERAL port (0)
349
+ // and close cleanly, and lets a real deploy choose its own port/host.
350
+ function createServer(opts = {}) {
351
+ return http.createServer(makeHandler(opts));
352
+ }
353
+
354
+ // Real "today" as a UTC YYYY-MM-DD — the ONLY impure call, isolated + injectable
355
+ // so the pipeline core (reconcilePayload) stays deterministic under test.
356
+ function todayISO() {
357
+ const d = new Date();
358
+ const pad = (n) => String(n).padStart(2, "0");
359
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
360
+ }
361
+
362
+ // CLI entry: `node trustledger/server.js [port]` (or PORT env). Never invoked by
363
+ // the tests (they create + listen themselves on an ephemeral port).
364
+ if (require.main === module) {
365
+ const port = Number(process.env.PORT || process.argv[2] || 8080);
366
+ const host = process.env.HOST || "127.0.0.1";
367
+ const srv = createServer();
368
+ srv.listen(port, host, () => {
369
+ process.stdout.write(
370
+ `TrustLedger web door listening on http://${host}:${port}/ (in-memory; nothing is written to disk)\n`
371
+ );
372
+ });
373
+ }
374
+
375
+ module.exports = {
376
+ createServer,
377
+ makeHandler,
378
+ // Re-exported from ./door-core (T-65.2) under the SAME names, so every existing
379
+ // consumer — the CLI `vh trust serve`, the tests, an embedding app — is
380
+ // untouched by the factoring.
381
+ reconcilePayload,
382
+ gatePayload,
383
+ WEB_PAID_FEATURE_ENTITLEMENTS,
384
+ inspectPayload,
385
+ indexHtml,
386
+ embeddedIndexHtml,
387
+ PUBLIC_INDEX,
388
+ HttpError,
389
+ MAX_BODY_BYTES,
390
+ todayISO,
391
+ };