verifyhash 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- package/trustledger/valueproof.js +350 -0
|
@@ -0,0 +1,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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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
|
+
};
|