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,796 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // trustledger/build-standalone.js — the DETERMINISTIC, OFFLINE, zero-third-party-dependency bundler
5
+ // that inlines the in-tree TrustLedger engine + web-door core into ONE self-contained HTML file:
6
+ //
7
+ // trustledger/dist/trustledger-standalone.html (the single-file OFFLINE app, FREE tier)
8
+ // trustledger/dist/trustledger-standalone.html.sha256 (the published `sha256sum -c` sidecar)
9
+ // trustledger/dist/BUILD-PROVENANCE.json (source->bundle provenance, verifier schema)
10
+ //
11
+ // It mirrors verifier/build-standalone.js's PROVEN technique exactly:
12
+ // * an EXPLICIT, FIXED module list (never a filesystem walk), inlined VERBATIM;
13
+ // * ONLY require() specifiers are rewritten, to a memoizing __require(id) CommonJS shim;
14
+ // * NO timestamp, NO randomness — the emitted bytes are a pure function of the committed sources,
15
+ // so two builds are BYTE-IDENTICAL and `--check` re-compiles everything from source and compares
16
+ // against the committed dist files (a stale bundle is a named MISMATCH, red in CI).
17
+ //
18
+ // WHY THIS EXISTS (T-65.2 / EPIC-65)
19
+ // The pilot-critical objection is DATA SENSITIVITY: a property-management broker will not upload
20
+ // real trust-account exports to someone's server. This file removes that objection completely: the
21
+ // human emails ONE .html file; the design partner double-clicks it, drags their three real exports
22
+ // in, and reads the SAME tie-out packet — with NO install and NO network. The privacy claim is not
23
+ // prose, it is checkable: the emitted file contains NO network API token at all (no fetch(, no
24
+ // XMLHttpRequest, no WebSocket, no EventSource, no sendBeacon, no dynamic import( — pinned by
25
+ // test/trustledger.standalone.test.js), so the browser devtools Network tab stays empty.
26
+ //
27
+ // WHAT THE EMITTED FILE CONTAINS
28
+ // (a) a DOM-FREE engine <script> between recognizable markers (__TRUSTLEDGER_ENGINE_BEGIN__ /
29
+ // __TRUSTLEDGER_ENGINE_END__): the __modules registry inlining ingest, match, reconcile, the
30
+ // policy pure path (its fs-backed bundled-policy loader swapped for the fixture JSON inlined
31
+ // at build time), close, report, the vendored pure-JS sha256, and the web door's payload core
32
+ // (door-core.js) VERBATIM. No document/window reference — a Node test extracts this block and
33
+ // evaluates it in `vm`, then drives the SAME payloads the server tests use to byte-identity.
34
+ // (b) the EXISTING drag-drop UI from trustledger/public/index.html, with its marked transport
35
+ // seams (the two fetch calls + two server-transport prose notes) swapped for direct in-page
36
+ // calls into the SAME door core the HTTP server routes to — the two surfaces cannot drift.
37
+ // (c) the T-29.3 license-gate MAPPING inlined VERBATIM (door-core.js is not re-implemented): a
38
+ // paid surface (per-state policy / seal) yields the SAME named license_required refusal the
39
+ // web door gives. The license VERIFIER is swapped for a FAIL-CLOSED shim (the offline app is
40
+ // the FREE tier and cannot verify a license), so a supplied license is REFUSED (named
41
+ // license_invalid pointing at the installed product) — the gate is never weakened, and no
42
+ // paid surface can ever be granted offline.
43
+ //
44
+ // OFFLINE + READ-ONLY: this builder reads the committed source files and writes ONLY under
45
+ // trustledger/dist/. It opens NO socket and makes NO network call.
46
+
47
+ const fs = require("fs");
48
+ const path = require("path");
49
+ const crypto = require("crypto");
50
+
51
+ const TL_DIR = __dirname;
52
+ const DIST_DIR = path.join(TL_DIR, "dist");
53
+ const OUT_PATH = path.join(DIST_DIR, "trustledger-standalone.html");
54
+ const SHA256_PATH = OUT_PATH + ".sha256";
55
+ const SHA256_BASENAME = path.basename(OUT_PATH);
56
+ const PROVENANCE_PATH = path.join(DIST_DIR, "BUILD-PROVENANCE.json");
57
+ const PROVENANCE_BASENAME = path.basename(PROVENANCE_PATH);
58
+ // The SAME version-free schema tag verifier/dist/BUILD-PROVENANCE.json uses.
59
+ const PROVENANCE_SCHEMA = "verifyhash/build-provenance@1";
60
+
61
+ // The drag-drop UI page whose marked transport seams this build swaps.
62
+ const PAGE_FILE = "public/index.html";
63
+ // The bundled per-state policy fixtures inlined (as JSON) into the policy-loader shim.
64
+ const POLICY_FIXTURES_DIR = path.join(TL_DIR, "fixtures", "policy");
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Deterministic file reading + hashing (same discipline as the verifier builder).
68
+ // ---------------------------------------------------------------------------
69
+
70
+ // Read a source file deterministically, normalizing line endings to "\n" (so a checkout with CRLF
71
+ // cannot change the emitted bytes) and stripping a leading shebang line.
72
+ function readSource(rel) {
73
+ let s = fs.readFileSync(path.join(TL_DIR, rel), "utf8");
74
+ s = s.replace(/\r\n/g, "\n");
75
+ s = s.replace(/^#![^\n]*\n/, "");
76
+ return s;
77
+ }
78
+
79
+ // sha256 hex of a utf8 string (the canonical hash unit for the bundle and its inlined sources).
80
+ function sha256HexOf(text) {
81
+ return crypto.createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
82
+ }
83
+
84
+ // The exact textual contents of the `.sha256` sidecar: one canonical line in the standard
85
+ // `sha256sum`/`shasum -a 256` format (`<hex>␠␠<basename>\n`) so a recipient can run
86
+ // `sha256sum -c trustledger-standalone.html.sha256` BEFORE opening the file.
87
+ function sha256SidecarFor(bundleText, basename) {
88
+ return `${sha256HexOf(bundleText)} ${basename}\n`;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // require() rewriting — identical technique to the verifier builder, but STRICTER: the engine block
93
+ // runs in a BROWSER <script>, so NO require may survive verbatim (there is no Node core in a page).
94
+ // Every specifier must be in the module's rewrite map; anything else is a hard build error.
95
+ // ---------------------------------------------------------------------------
96
+
97
+ function requireSpecifiers(src) {
98
+ return [...src.matchAll(/require\(\s*["']([^"']+)["']\s*\)/g)].map((m) => m[1]);
99
+ }
100
+
101
+ function rewriteRequires(src, rewrite, idForError) {
102
+ for (const spec of requireSpecifiers(src)) {
103
+ if (!Object.prototype.hasOwnProperty.call(rewrite, spec)) {
104
+ throw new Error(
105
+ `build-standalone: module "${idForError}" has an un-inlined require(${JSON.stringify(spec)}). ` +
106
+ "The browser bundle can require NOTHING — add it to the module's rewrite map (and inline its target)."
107
+ );
108
+ }
109
+ }
110
+ return src.replace(/require\(\s*["']([^"']+)["']\s*\)/g, (_full, spec) => {
111
+ return `__require(${JSON.stringify(rewrite[spec])})`;
112
+ });
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // SYNTHETIC module bodies (the ONLY two swapped bodies; everything else is inlined verbatim).
117
+ // ---------------------------------------------------------------------------
118
+
119
+ // (1) The bundled-policy loader shim: the SAME { BUNDLED_DIR, listBundledPolicyNames,
120
+ // readBundledPolicyFile } surface as trustledger/lib/policy-bundled-loader.js (the module's SOLE
121
+ // impure seam, isolated by T-65.1 precisely so this build could swap it), backed by the committed
122
+ // per-state policy fixture JSON inlined below at build time. Deterministic: filenames sorted; the
123
+ // object's key order is that sorted order. policy.js keeps ALL validation/sorting/PolicyError logic.
124
+ function policyLoaderShimBody() {
125
+ const names = fs
126
+ .readdirSync(POLICY_FIXTURES_DIR)
127
+ .filter((n) => n.endsWith(".json"))
128
+ .sort();
129
+ const bundled = {};
130
+ for (const n of names) {
131
+ bundled[n] = fs.readFileSync(path.join(POLICY_FIXTURES_DIR, n), "utf8").replace(/\r\n/g, "\n");
132
+ }
133
+ return [
134
+ '"use strict";',
135
+ "// Inlined bundled-policy provider for the standalone app: the SAME surface as",
136
+ "// trustledger/lib/policy-bundled-loader.js (the policy module's isolated fs seam, T-65.1),",
137
+ "// backed by the committed trustledger/fixtures/policy/*.json inlined verbatim at build time.",
138
+ "// No fs, no path — browser-safe. policy.js keeps every validation/ordering/PolicyError rule.",
139
+ `var BUNDLED = ${JSON.stringify(bundled)};`,
140
+ 'var BUNDLED_DIR = "(bundled policies inlined from trustledger/fixtures/policy)";',
141
+ "function listBundledPolicyNames() {",
142
+ " return Object.keys(BUNDLED);",
143
+ "}",
144
+ "function readBundledPolicyFile(name) {",
145
+ " if (!Object.prototype.hasOwnProperty.call(BUNDLED, name)) {",
146
+ ' throw new Error("no bundled policy named " + name);',
147
+ " }",
148
+ ' return { full: BUNDLED_DIR + "/" + name, text: BUNDLED[name] };',
149
+ "}",
150
+ "module.exports = {",
151
+ " BUNDLED_DIR: BUNDLED_DIR,",
152
+ " listBundledPolicyNames: listBundledPolicyNames,",
153
+ " readBundledPolicyFile: readBundledPolicyFile,",
154
+ "};",
155
+ ].join("\n");
156
+ }
157
+
158
+ // (2) The FAIL-CLOSED offline license shim, replacing trustledger/license.js in the bundle. The
159
+ // offline app is the FREE tier: license verification (and every paid surface it unlocks) runs in
160
+ // the installed TrustLedger product. door-core.js's gate is inlined VERBATIM above this shim, so:
161
+ // * a paid request with NO license -> the gate's own named license_required refusal, byte-for-
162
+ // byte the SAME as the web door's (the gate never reaches this shim on that path);
163
+ // * a paid request WITH a license -> readLicense throws here, which the verbatim gate wraps in
164
+ // its named license_invalid refusal pointing at the installed product. NOTHING here can return
165
+ // a valid verdict, so the offline app can NEVER grant a paid surface — fail closed, gate REUSED
166
+ // and never weakened.
167
+ const LICENSE_SHIM_BODY = [
168
+ '"use strict";',
169
+ "// OFFLINE license shim (see trustledger/build-standalone.js): the free-tier standalone app",
170
+ "// cannot verify a license — paid surfaces run in the installed TrustLedger product. Fail closed.",
171
+ "var OFFLINE_REASON =",
172
+ ' "license verification runs in the installed TrustLedger product; this offline app is the " +',
173
+ ' "free tier (baseline reconcile + file inspect only)";',
174
+ "function readLicense() {",
175
+ " throw new Error(OFFLINE_REASON);",
176
+ "}",
177
+ "function verifyLicense() {",
178
+ ' return { valid: false, reason: "offline_free_tier", entitlements: [] };',
179
+ "}",
180
+ "function hasEntitlement() {",
181
+ " return false;",
182
+ "}",
183
+ "module.exports = {",
184
+ " readLicense: readLicense,",
185
+ " verifyLicense: verifyLicense,",
186
+ " hasEntitlement: hasEntitlement,",
187
+ "};",
188
+ ].join("\n");
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // The EXPLICIT, FIXED engine module list. Order is deterministic by construction (hand-listed).
192
+ // Each entry mirrors the verifier builder: { id, file, rewrite, body?, entry? } — `body` (a string
193
+ // or a zero-arg function returning one) REPLACES the file's source (the two synthetic shims above);
194
+ // otherwise the file is inlined VERBATIM with only its require() specifiers rewritten.
195
+ // ---------------------------------------------------------------------------
196
+
197
+ const ENGINE_MODULES = [
198
+ // The vendored pure-JS sha256 (T-65.1) — requires nothing.
199
+ { id: "sha256-vendored", file: "lib/sha256-vendored.js", rewrite: {} },
200
+ // The policy module's isolated fs seam — body SWAPPED for the inlined-fixture shim.
201
+ {
202
+ id: "policy-bundled-loader",
203
+ file: "lib/policy-bundled-loader.js",
204
+ rewrite: {},
205
+ body: policyLoaderShimBody,
206
+ },
207
+ // The pure pipeline, inlined verbatim.
208
+ { id: "reconcile", file: "reconcile.js", rewrite: {} },
209
+ {
210
+ id: "policy",
211
+ file: "policy.js",
212
+ rewrite: { "./reconcile": "reconcile", "./lib/policy-bundled-loader": "policy-bundled-loader" },
213
+ },
214
+ { id: "match", file: "match.js", rewrite: {} },
215
+ { id: "ingest", file: "ingest.js", rewrite: {} },
216
+ { id: "close", file: "close.js", rewrite: { "./lib/sha256-vendored": "sha256-vendored" } },
217
+ {
218
+ id: "report",
219
+ file: "report.js",
220
+ rewrite: {
221
+ "./ingest": "ingest",
222
+ "./match": "match",
223
+ "./reconcile": "reconcile",
224
+ "./policy": "policy",
225
+ "./close": "close",
226
+ },
227
+ },
228
+ // The license module — body SWAPPED for the fail-closed offline shim (free tier only).
229
+ { id: "license", file: "license.js", rewrite: {}, body: LICENSE_SHIM_BODY },
230
+ // The web door's payload core, inlined VERBATIM and LAST — the entry the page calls.
231
+ {
232
+ id: "door-core",
233
+ file: "door-core.js",
234
+ rewrite: {
235
+ "./ingest": "ingest",
236
+ "./report": "report",
237
+ "./policy": "policy",
238
+ "./close": "close",
239
+ "./license": "license",
240
+ },
241
+ entry: true,
242
+ },
243
+ ];
244
+
245
+ // Resolve a module's inlined body text (synthetic body, or verbatim source with rewrites).
246
+ function bodyOf(m) {
247
+ if (m.body != null) {
248
+ return typeof m.body === "function" ? m.body() : m.body;
249
+ }
250
+ return rewriteRequires(readSource(m.file), m.rewrite, m.id);
251
+ }
252
+
253
+ // The tiny CommonJS shim the engine block embeds (byte-identical to the verifier builder's).
254
+ const COMMONJS_SHIM = [
255
+ "// ---- minimal CommonJS module shim (so the inlined modules keep their require() structure) --------",
256
+ "var __modules = Object.create(null);",
257
+ "var __cache = Object.create(null);",
258
+ "function __require(id) {",
259
+ " if (id in __cache) return __cache[id].exports;",
260
+ " var factory = __modules[id];",
261
+ " if (!factory) throw new Error('standalone bundle: unknown module: ' + id);",
262
+ " var module = { exports: {} };",
263
+ " __cache[id] = module;",
264
+ " factory(module, module.exports, __require);",
265
+ " return module.exports;",
266
+ "}",
267
+ ];
268
+
269
+ // The recognizable engine-block markers a Node test extracts + vm-evaluates the block by.
270
+ const ENGINE_BEGIN_MARKER = "// __TRUSTLEDGER_ENGINE_BEGIN__";
271
+ const ENGINE_END_MARKER = "// __TRUSTLEDGER_ENGINE_END__";
272
+
273
+ // Build the DOM-free engine <script> block. Everything between the markers is plain computation —
274
+ // no DOM, no network, no clock — so `vm.runInNewContext` proves it needs nothing a browser page
275
+ // would provide. The block defines ONE global, TrustLedgerStandalone: { door, engine }.
276
+ function engineScriptText() {
277
+ const parts = [];
278
+ parts.push("<script>");
279
+ parts.push(ENGINE_BEGIN_MARKER);
280
+ parts.push('"use strict";');
281
+ parts.push("var TrustLedgerStandalone = (function () {");
282
+ parts.push(COMMONJS_SHIM.join("\n"));
283
+ parts.push("");
284
+
285
+ let entryId = null;
286
+ for (const m of ENGINE_MODULES) {
287
+ if (m.entry) entryId = m.id;
288
+ parts.push(`// ===== module: ${m.id} (from trustledger/${m.file}) =====`);
289
+ parts.push(`__modules[${JSON.stringify(m.id)}] = function (module, exports, __require) {`);
290
+ parts.push(bodyOf(m));
291
+ parts.push("};");
292
+ parts.push("");
293
+ }
294
+ if (!entryId) throw new Error("build-standalone: no entry module declared");
295
+
296
+ parts.push("return {");
297
+ parts.push(` door: __require(${JSON.stringify(entryId)}),`);
298
+ parts.push(" engine: {");
299
+ parts.push(' ingest: __require("ingest"),');
300
+ parts.push(' match: __require("match"),');
301
+ parts.push(' reconcile: __require("reconcile"),');
302
+ parts.push(' policy: __require("policy"),');
303
+ parts.push(' close: __require("close"),');
304
+ parts.push(' report: __require("report"),');
305
+ parts.push(' sha256: __require("sha256-vendored"),');
306
+ parts.push(" },");
307
+ parts.push("};");
308
+ parts.push("})();");
309
+ parts.push(ENGINE_END_MARKER);
310
+ parts.push("</scr" + "ipt>");
311
+ return parts.join("\n");
312
+ }
313
+
314
+ // The offline UI glue <script>: the bridge between the page's transport seams and the in-page door.
315
+ // It mirrors server.js EXACTLY — sendError's { error, message } envelope for a named HttpError, the
316
+ // generic internal_error otherwise (never a stack trace), and the same UTC YYYY-MM-DD todayISO()
317
+ // computes — wrapped in a resolved Promise so the UI's .then/.catch chains behave exactly as they
318
+ // did over the network door.
319
+ function glueScriptText() {
320
+ return [
321
+ "<script>",
322
+ "// ---- OFFLINE GLUE (build-generated; T-65.2): routes the page's transport seams into the",
323
+ "// in-page engine above. Mirrors trustledger/server.js: the { error, message } envelope of",
324
+ "// sendError for a named HttpError, internal_error otherwise, and todayISO()'s UTC date.",
325
+ '"use strict";',
326
+ "function __tlOfflineApi(fn) {",
327
+ " return Promise.resolve().then(function () {",
328
+ " try {",
329
+ " return { ok: true, data: fn(TrustLedgerStandalone.door, __tlTodayISO) };",
330
+ " } catch (err) {",
331
+ " if (err instanceof TrustLedgerStandalone.door.HttpError) {",
332
+ " return { ok: false, data: { error: err.code, message: err.message } };",
333
+ " }",
334
+ ' return { ok: false, data: { error: "internal_error", message: "an internal error occurred" } };',
335
+ " }",
336
+ " });",
337
+ "}",
338
+ "function __tlTodayISO() {",
339
+ " var d = new Date();",
340
+ ' var pad = function (n) { return String(n).padStart(2, "0"); };',
341
+ ' return d.getUTCFullYear() + "-" + pad(d.getUTCMonth() + 1) + "-" + pad(d.getUTCDate());',
342
+ "}",
343
+ "</scr" + "ipt>",
344
+ ].join("\n");
345
+ }
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // The marked transport seams in public/index.html and their OFFLINE replacements.
349
+ // Each seam is a contiguous region between `__TL_TRANSPORT_SEAM:<NAME>:BEGIN__` and
350
+ // `...:END__` marker lines; the replacement swaps the WHOLE region (markers included), so no
351
+ // marker token — and no `fetch(` token — survives into the emitted file.
352
+ // ---------------------------------------------------------------------------
353
+
354
+ const SEAM_REPLACEMENTS = [
355
+ {
356
+ name: "NOTE",
357
+ replacement: [
358
+ '<p class="note">Drop your three monthly files. Everything runs INSIDE this one',
359
+ "file: your browser reads the files and the reconciliation executes right here on",
360
+ "this page. No server is contacted — this file contains no network API at all",
361
+ "(check your browser devtools Network tab yourself) — so your trust-account data",
362
+ "never leaves this machine. The result, including the downloadable HTML and CSV",
363
+ "audit packet, renders below.</p>",
364
+ ].join("\n"),
365
+ },
366
+ {
367
+ name: "LICENSE_NOTE",
368
+ replacement: [
369
+ ' <p class="note">Per-state policy packs and the tamper-evident seal are paid',
370
+ " features that run in the INSTALLED TrustLedger product. This offline app is the",
371
+ " free tier — the baseline reconcile and file inspection need no license — and it",
372
+ " cannot verify a license, so requesting a paid feature here is refused with the",
373
+ " same named notice the web door gives.</p>",
374
+ ].join("\n"),
375
+ },
376
+ {
377
+ name: "INSPECT",
378
+ replacement: [
379
+ " // OFFLINE build (T-65.2): no network transport — call the SAME door core",
380
+ " // (trustledger/door-core.js, inlined in the engine block above) directly.",
381
+ " return __tlOfflineApi(function (door, today) { return door.inspectPayload(body); });",
382
+ ].join("\n"),
383
+ },
384
+ {
385
+ name: "RECONCILE",
386
+ replacement: [
387
+ " // OFFLINE build (T-65.2): no network transport — call the SAME door core",
388
+ " // (trustledger/door-core.js, inlined in the engine block above) directly.",
389
+ " return __tlOfflineApi(function (door, today) { return door.reconcilePayload(body, today()); });",
390
+ ].join("\n"),
391
+ },
392
+ ];
393
+
394
+ // Replace ONE marked seam region (marker lines included) with its replacement text. STRICT: each
395
+ // marker must appear exactly once, BEGIN before END — a moved/removed marker is a hard build error,
396
+ // never a silently-unswapped transport.
397
+ function replaceSeam(pageText, name, replacement) {
398
+ const begin = `__TL_TRANSPORT_SEAM:${name}:BEGIN__`;
399
+ const end = `__TL_TRANSPORT_SEAM:${name}:END__`;
400
+ for (const marker of [begin, end]) {
401
+ const first = pageText.indexOf(marker);
402
+ if (first === -1) {
403
+ throw new Error(`build-standalone: seam marker ${marker} not found in ${PAGE_FILE}`);
404
+ }
405
+ if (pageText.indexOf(marker, first + marker.length) !== -1) {
406
+ throw new Error(`build-standalone: seam marker ${marker} appears more than once in ${PAGE_FILE}`);
407
+ }
408
+ }
409
+ const beginAt = pageText.indexOf(begin);
410
+ const endAt = pageText.indexOf(end);
411
+ if (endAt < beginAt) {
412
+ throw new Error(`build-standalone: seam ${name} END marker precedes BEGIN in ${PAGE_FILE}`);
413
+ }
414
+ // The region spans from the START of the line carrying BEGIN to the END of the line carrying END.
415
+ const regionStart = pageText.lastIndexOf("\n", beginAt) + 1;
416
+ const lineEnd = pageText.indexOf("\n", endAt);
417
+ const regionEnd = lineEnd === -1 ? pageText.length : lineEnd + 1;
418
+ return pageText.slice(0, regionStart) + replacement + "\n" + pageText.slice(regionEnd);
419
+ }
420
+
421
+ // The fixed generated-file banner (NO timestamp -> deterministic), inserted right after <!doctype>.
422
+ const GENERATED_BANNER = [
423
+ "<!--",
424
+ " trustledger-standalone.html — the SINGLE-FILE, OFFLINE TrustLedger app (FREE tier).",
425
+ "",
426
+ " GENERATED by trustledger/build-standalone.js from the in-tree engine — DO NOT EDIT BY HAND.",
427
+ " Re-generate with: node trustledger/build-standalone.js (deterministic; `--check` attests the",
428
+ " committed file reproduces byte-for-byte from source, see trustledger/dist/BUILD-PROVENANCE.json).",
429
+ "",
430
+ " HOW TO USE IT (no install, no account, no server): save this ONE file, double-click it, drag",
431
+ " your bank statement, ledger, and rent-roll exports in. Everything runs inside the page: the",
432
+ " file contains NO network API, so your trust-account data never leaves this machine (verify in",
433
+ " your browser devtools Network tab).",
434
+ "",
435
+ " FREE TIER + HONEST POSTURE: the baseline three-way reconcile and file inspection only.",
436
+ " Per-state policy packs, the tamper-evident seal, and license verification run in the installed",
437
+ " TrustLedger product; requesting them here is refused with the same named notice the web door",
438
+ " gives. This tool AIDS reconciliation — the broker remains the legal trust-account custodian,",
439
+ " and a CPA's review still governs.",
440
+ "-->",
441
+ ].join("\n");
442
+
443
+ // ---------------------------------------------------------------------------
444
+ // Build the FULL standalone HTML text. Pure function of the committed sources -> deterministic.
445
+ // ---------------------------------------------------------------------------
446
+
447
+ function buildHtml() {
448
+ let page = readSource(PAGE_FILE);
449
+
450
+ // (1) Swap each marked transport seam for its offline replacement (markers removed).
451
+ for (const seam of SEAM_REPLACEMENTS) {
452
+ page = replaceSeam(page, seam.name, seam.replacement);
453
+ }
454
+
455
+ // (2) Insert the generated-file banner right after the doctype line.
456
+ const doctype = "<!doctype html>\n";
457
+ if (!page.startsWith(doctype)) {
458
+ throw new Error(`build-standalone: ${PAGE_FILE} must start with "<!doctype html>"`);
459
+ }
460
+ page = doctype + GENERATED_BANNER + "\n" + page.slice(doctype.length);
461
+
462
+ // (3) Insert the engine block + offline glue immediately BEFORE the page's own UI <script>, so
463
+ // the UI's swapped seams find TrustLedgerStandalone / __tlOfflineApi already defined.
464
+ const uiScriptAnchor = "<script>\n(function () {";
465
+ const anchorAt = page.indexOf(uiScriptAnchor);
466
+ if (anchorAt === -1 || page.indexOf(uiScriptAnchor, anchorAt + 1) !== -1) {
467
+ throw new Error(`build-standalone: expected exactly one UI script anchor in ${PAGE_FILE}`);
468
+ }
469
+ page =
470
+ page.slice(0, anchorAt) +
471
+ engineScriptText() +
472
+ "\n" +
473
+ glueScriptText() +
474
+ "\n" +
475
+ page.slice(anchorAt);
476
+
477
+ return page;
478
+ }
479
+
480
+ // ---------------------------------------------------------------------------
481
+ // Build provenance — the SAME schema (verifyhash/build-provenance@1) and module-record shape as
482
+ // verifier/dist/BUILD-PROVENANCE.json, so a reviewer reads both manifests the same way. Each inlined
483
+ // module is pinned by the sha256 of its NORMALIZED source (the exact bytes the build reads) AND of
484
+ // the post-rewrite text actually placed in the bundle; the two synthetic shim bodies are marked
485
+ // `synthetic` with their inlined hash (their logic lives in THIS builder, not a source file). The
486
+ // UI page the seams are swapped into is pinned separately under `page`.
487
+ // ---------------------------------------------------------------------------
488
+
489
+ function moduleProvenance(m) {
490
+ if (m.body != null) {
491
+ const body = typeof m.body === "function" ? m.body() : m.body;
492
+ return {
493
+ id: m.id,
494
+ synthetic: true,
495
+ sourceFile: null,
496
+ sourceSha256: null,
497
+ inlinedSha256: sha256HexOf(body),
498
+ note:
499
+ m.id === "policy-bundled-loader"
500
+ ? "swapped body (bundled-policy loader shim; inlines trustledger/fixtures/policy/*.json) — defined in build-standalone.js, not a source file"
501
+ : "swapped body (fail-closed offline license shim) — defined in build-standalone.js, not a source file",
502
+ };
503
+ }
504
+ const src = readSource(m.file);
505
+ return {
506
+ id: m.id,
507
+ synthetic: false,
508
+ sourceFile: `trustledger/${m.file}`,
509
+ sourceSha256: sha256HexOf(src),
510
+ inlinedSha256: sha256HexOf(rewriteRequires(src, m.rewrite, m.id)),
511
+ entry: m.entry === true,
512
+ };
513
+ }
514
+
515
+ function buildProvenanceObject() {
516
+ const bundleText = buildHtml();
517
+ return {
518
+ schema: PROVENANCE_SCHEMA,
519
+ description:
520
+ "Maps the published TrustLedger standalone offline app's sha256 to the ordered, individually-hashed " +
521
+ "in-tree source files it inlines. Reproduce + attest the whole chain offline with: " +
522
+ "node trustledger/build-standalone.js --check",
523
+ targets: {
524
+ "trustledger-standalone": {
525
+ bundle: SHA256_BASENAME,
526
+ sidecar: path.basename(SHA256_PATH),
527
+ bundleBytes: Buffer.byteLength(bundleText, "utf8"),
528
+ bundleSha256: sha256HexOf(bundleText),
529
+ sidecarLine: sha256SidecarFor(bundleText, SHA256_BASENAME).trim(),
530
+ // The UI page whose marked transport seams the build swaps (pinned like any other source).
531
+ page: {
532
+ sourceFile: `trustledger/${PAGE_FILE}`,
533
+ sourceSha256: sha256HexOf(readSource(PAGE_FILE)),
534
+ },
535
+ // The ORDERED inlined engine modules — the exact composition a skeptic re-hashes source against.
536
+ modules: ENGINE_MODULES.map(moduleProvenance),
537
+ },
538
+ },
539
+ };
540
+ }
541
+
542
+ function buildProvenanceText() {
543
+ return JSON.stringify(buildProvenanceObject(), null, 2) + "\n";
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // Writers
548
+ // ---------------------------------------------------------------------------
549
+
550
+ function writeAll() {
551
+ const html = buildHtml();
552
+ const sidecar = sha256SidecarFor(html, SHA256_BASENAME);
553
+ const provenance = buildProvenanceText();
554
+ fs.mkdirSync(DIST_DIR, { recursive: true });
555
+ fs.writeFileSync(OUT_PATH, html);
556
+ fs.writeFileSync(SHA256_PATH, sidecar);
557
+ fs.writeFileSync(PROVENANCE_PATH, provenance);
558
+ return { html, sidecar, provenance };
559
+ }
560
+
561
+ // ---------------------------------------------------------------------------
562
+ // REPRODUCE-AND-ATTEST (`--check`) — same posture as verifier/build-standalone.js --check: re-compile
563
+ // the bundle from the in-tree source a skeptic can READ, recompute the published checksum + the
564
+ // provenance manifest, and assert the COMMITTED files are byte-for-byte what that source compiles to.
565
+ // Read-only; writes NOTHING; a stale/tampered dist is a named MISMATCH (exit 1), never a crash.
566
+ // ---------------------------------------------------------------------------
567
+
568
+ function checkBundle() {
569
+ const rel = (p) => path.relative(TL_DIR, p);
570
+ const result = {
571
+ bundlePath: rel(OUT_PATH),
572
+ sha256Path: rel(SHA256_PATH),
573
+ expectedHex: null,
574
+ bundle: { ok: false, reason: "" },
575
+ sidecar: { ok: false, reason: "" },
576
+ sources: { ok: true, reason: "", offenders: [] },
577
+ };
578
+
579
+ // Source-presence FIRST, so a missing inlined source is a named MISMATCH, not a build crash.
580
+ const sourceFiles = [PAGE_FILE, ...ENGINE_MODULES.filter((m) => m.body == null).map((m) => m.file)];
581
+ for (const f of sourceFiles) {
582
+ if (!fs.existsSync(path.join(TL_DIR, f))) {
583
+ result.sources.ok = false;
584
+ result.sources.offenders.push({ sourceFile: `trustledger/${f}`, reason: "MISSING" });
585
+ }
586
+ }
587
+ result.sources.reason = result.sources.offenders.length
588
+ ? `inlined source(s) MISSING: ${result.sources.offenders.map((o) => o.sourceFile).join(", ")}`
589
+ : `all ${sourceFiles.length} inlined source files present`;
590
+
591
+ let expectedHtml, expectedBuf, expectedSidecar;
592
+ try {
593
+ expectedHtml = buildHtml();
594
+ expectedBuf = Buffer.from(expectedHtml, "utf8");
595
+ result.expectedHex = sha256HexOf(expectedHtml);
596
+ expectedSidecar = sha256SidecarFor(expectedHtml, SHA256_BASENAME);
597
+ } catch (e) {
598
+ const why = `cannot recompile ${result.bundlePath} from source: ${e && e.message ? e.message : e}`;
599
+ result.bundle.reason = why;
600
+ result.sidecar.reason = why;
601
+ result.ok = false;
602
+ return result;
603
+ }
604
+
605
+ if (!fs.existsSync(OUT_PATH)) {
606
+ result.bundle.reason = `committed bundle ${result.bundlePath} is MISSING`;
607
+ } else {
608
+ const committed = fs.readFileSync(OUT_PATH);
609
+ if (committed.equals(expectedBuf)) {
610
+ result.bundle.ok = true;
611
+ result.bundle.reason = `recomputed bytes == committed bytes (sha256 ${result.expectedHex})`;
612
+ } else {
613
+ const committedHex = crypto.createHash("sha256").update(committed).digest("hex");
614
+ result.bundle.reason =
615
+ `committed bundle does NOT reproduce from source ` +
616
+ `(committed sha256 ${committedHex} != recomputed ${result.expectedHex})`;
617
+ }
618
+ }
619
+
620
+ if (!fs.existsSync(SHA256_PATH)) {
621
+ result.sidecar.reason = `committed sidecar ${result.sha256Path} is MISSING`;
622
+ } else {
623
+ const committedSidecar = fs.readFileSync(SHA256_PATH, "utf8");
624
+ if (committedSidecar === expectedSidecar) {
625
+ result.sidecar.ok = true;
626
+ result.sidecar.reason = `published hex == recomputed hex (${result.expectedHex})`;
627
+ } else {
628
+ result.sidecar.reason =
629
+ `committed sidecar does NOT match the recomputed published line ` +
630
+ `(expected "${expectedSidecar.trim()}", got "${committedSidecar.trim()}")`;
631
+ }
632
+ }
633
+
634
+ result.ok = result.bundle.ok && result.sidecar.ok && result.sources.ok;
635
+ return result;
636
+ }
637
+
638
+ // Reproduce the manifest from source AND cross-check every source hash the COMMITTED manifest pins
639
+ // (page + every non-synthetic module) against the file on disk — a one-byte change to ANY inlined
640
+ // source is named precisely by its own filename against the published pin.
641
+ function checkProvenance() {
642
+ const rel = (p) => path.relative(TL_DIR, p);
643
+ const result = {
644
+ manifestPath: rel(PROVENANCE_PATH),
645
+ manifest: { ok: false, reason: "" },
646
+ chain: { ok: true, reason: "", offenders: [] },
647
+ };
648
+
649
+ let expectedText = null;
650
+ let expectedObj = { targets: {} };
651
+ try {
652
+ expectedText = buildProvenanceText();
653
+ expectedObj = buildProvenanceObject();
654
+ } catch (e) {
655
+ result.manifest.reason = `cannot recompute ${result.manifestPath} from source: ${
656
+ e && e.message ? e.message : e
657
+ }`;
658
+ }
659
+
660
+ let committedObj = null;
661
+ if (!fs.existsSync(PROVENANCE_PATH)) {
662
+ result.manifest.reason = `committed manifest ${result.manifestPath} is MISSING`;
663
+ } else {
664
+ const committed = fs.readFileSync(PROVENANCE_PATH, "utf8");
665
+ try {
666
+ committedObj = JSON.parse(committed);
667
+ } catch (_) {
668
+ committedObj = null;
669
+ }
670
+ if (expectedText === null) {
671
+ // recompute failed; reason already set — the chain below still pins from the committed copy.
672
+ } else if (committed === expectedText) {
673
+ result.manifest.ok = true;
674
+ result.manifest.reason = `recomputed manifest == committed manifest (sha256 ${sha256HexOf(expectedText)})`;
675
+ } else {
676
+ result.manifest.reason =
677
+ `committed manifest does NOT reproduce from source ` +
678
+ `(committed sha256 ${sha256HexOf(committed)} != recomputed ${sha256HexOf(expectedText)})`;
679
+ }
680
+ }
681
+
682
+ // Pin against the COMMITTED manifest (what was published); fall back to the recomputed one.
683
+ const pinSource = committedObj && committedObj.targets ? committedObj : expectedObj;
684
+ const pinned = new Map();
685
+ for (const target of Object.values(pinSource.targets || {})) {
686
+ if (target.page && target.page.sourceFile) {
687
+ pinned.set(target.page.sourceFile, { id: "ui-page", sha256: target.page.sourceSha256 });
688
+ }
689
+ for (const mod of target.modules || []) {
690
+ if (mod.synthetic || !mod.sourceFile) continue;
691
+ if (!pinned.has(mod.sourceFile)) pinned.set(mod.sourceFile, { id: mod.id, sha256: mod.sourceSha256 });
692
+ }
693
+ }
694
+ for (const [sourceFile, pin] of pinned) {
695
+ const relFile = sourceFile.replace(/^trustledger\//, "");
696
+ const abs = path.join(TL_DIR, relFile);
697
+ const onDisk = fs.existsSync(abs) ? sha256HexOf(readSource(relFile)) : null;
698
+ if (onDisk !== pin.sha256) {
699
+ result.chain.offenders.push({
700
+ id: pin.id,
701
+ sourceFile,
702
+ expected: pin.sha256,
703
+ got: onDisk === null ? "MISSING" : onDisk,
704
+ });
705
+ result.chain.ok = false;
706
+ }
707
+ }
708
+ result.chain.reason = result.chain.offenders.length
709
+ ? `source(s) do NOT match the manifest's pinned sha256: ` +
710
+ result.chain.offenders
711
+ .map(
712
+ (o) =>
713
+ `${o.sourceFile} (pinned ${String(o.expected).slice(0, 12)}…, got ${
714
+ o.got === "MISSING" ? "MISSING" : o.got.slice(0, 12) + "…"
715
+ })`
716
+ )
717
+ .join("; ")
718
+ : "every inlined source file hashes to its manifest-pinned sha256";
719
+
720
+ result.ok = result.manifest.ok && result.chain.ok;
721
+ return result;
722
+ }
723
+
724
+ function runCheck(io) {
725
+ const out = (io && io.write) || ((s) => process.stdout.write(s));
726
+ const err = (io && io.writeErr) || ((s) => process.stderr.write(s));
727
+
728
+ out("trustledger standalone REPRODUCE-AND-ATTEST (--check): re-compiling the offline app from in-tree\n");
729
+ out("source, recomputing its published checksum + build-provenance manifest, and comparing all against\n");
730
+ out("the committed files. No network; no writes.\n\n");
731
+
732
+ let allOk = true;
733
+
734
+ const b = checkBundle();
735
+ out(`[${b.bundle.ok ? "MATCH" : "MISMATCH"}] bundle ${b.bundlePath}: ${b.bundle.reason}\n`);
736
+ out(`[${b.sidecar.ok ? "MATCH" : "MISMATCH"}] sidecar ${b.sha256Path}: ${b.sidecar.reason}\n`);
737
+ out(`[${b.sources.ok ? "MATCH" : "MISMATCH"}] sources ${b.bundlePath}: ${b.sources.reason}\n`);
738
+ if (!b.ok) allOk = false;
739
+
740
+ const p = checkProvenance();
741
+ out(`[${p.manifest.ok ? "MATCH" : "MISMATCH"}] manifest ${p.manifestPath}: ${p.manifest.reason}\n`);
742
+ out(`[${p.chain.ok ? "MATCH" : "MISMATCH"}] sources->manifest: ${p.chain.reason}\n`);
743
+ if (!p.ok) allOk = false;
744
+
745
+ if (allOk) {
746
+ out("\nALL MATCH — the committed offline app, its sidecar AND the build-provenance manifest reproduce\n");
747
+ out("byte-for-byte from the in-tree source, and every inlined source file hashes to its pinned sha256.\n");
748
+ return 0;
749
+ }
750
+ err("\nMISMATCH — at least one committed file does NOT reproduce from source (see above). Re-run\n");
751
+ err("`node trustledger/build-standalone.js` (no flag) to regenerate, or distrust this checkout.\n");
752
+ return 1;
753
+ }
754
+
755
+ if (require.main === module) {
756
+ if (process.argv.slice(2).includes("--check")) {
757
+ process.exit(runCheck());
758
+ }
759
+ const { html, sidecar, provenance } = writeAll();
760
+ process.stdout.write(`wrote ${path.relative(TL_DIR, OUT_PATH)} (${Buffer.byteLength(html)} bytes)\n`);
761
+ process.stdout.write(`wrote ${path.relative(TL_DIR, SHA256_PATH)} (${sidecar.trim()})\n`);
762
+ process.stdout.write(
763
+ `wrote ${path.relative(TL_DIR, PROVENANCE_PATH)} (${Buffer.byteLength(provenance)} bytes)\n`
764
+ );
765
+ }
766
+
767
+ module.exports = {
768
+ buildHtml,
769
+ buildProvenanceObject,
770
+ buildProvenanceText,
771
+ moduleProvenance,
772
+ engineScriptText,
773
+ glueScriptText,
774
+ policyLoaderShimBody,
775
+ LICENSE_SHIM_BODY,
776
+ SEAM_REPLACEMENTS,
777
+ replaceSeam,
778
+ sha256HexOf,
779
+ sha256SidecarFor,
780
+ writeAll,
781
+ checkBundle,
782
+ checkProvenance,
783
+ runCheck,
784
+ ENGINE_MODULES,
785
+ ENGINE_BEGIN_MARKER,
786
+ ENGINE_END_MARKER,
787
+ OUT_PATH,
788
+ SHA256_PATH,
789
+ SHA256_BASENAME,
790
+ PROVENANCE_PATH,
791
+ PROVENANCE_BASENAME,
792
+ PROVENANCE_SCHEMA,
793
+ DIST_DIR,
794
+ TL_DIR,
795
+ PAGE_FILE,
796
+ };