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,398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// trustledger/policy.js — versioned, strictly-validated per-state trust-rule
|
|
5
|
+
// policy + a PURE applyPolicy() that overrides exception severities.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS EXISTS
|
|
9
|
+
// ---------------
|
|
10
|
+
// reconcile.js ships a single, hard-coded DEFAULT_SEVERITY baseline. But what
|
|
11
|
+
// counts as "out of trust" vs. "needs a human eye" is a matter of the STATE's
|
|
12
|
+
// trust-account statute (e.g. some states make an owner draw against tenant
|
|
13
|
+
// money a per-se ERROR; others treat an NSF reversal as merely a warning until
|
|
14
|
+
// it is re-deposited). A policy lets a CPA/broker pin each exception type to the
|
|
15
|
+
// severity their jurisdiction's rule demands, and to CITE the statute so the
|
|
16
|
+
// control is defensible in an audit.
|
|
17
|
+
//
|
|
18
|
+
// HONEST POSTURE / DISCLAIMER
|
|
19
|
+
// ---------------------------
|
|
20
|
+
// A policy file is an AID to reconciliation. Editing severities here does NOT
|
|
21
|
+
// make the result legal advice and does NOT discharge the broker's duty as the
|
|
22
|
+
// responsible legal custodian of trust funds, nor does it replace a CPA's
|
|
23
|
+
// review. The shipped fixtures are DRAFT / NOT-LEGAL-ADVICE skeletons a
|
|
24
|
+
// qualified human edits. This module makes NO claim of regulatory compliance.
|
|
25
|
+
//
|
|
26
|
+
// DESIGN PROPERTIES
|
|
27
|
+
// -----------------
|
|
28
|
+
// * PURE: readPolicy/validatePolicy/applyPolicy have no clock, no I/O, no
|
|
29
|
+
// hidden state; the same inputs always produce byte-identical output.
|
|
30
|
+
// LOADING this module executes NO impure require at all (browser-portable);
|
|
31
|
+
// the ONLY filesystem code — the bundled-fixture loader — is isolated in
|
|
32
|
+
// ./lib/policy-bundled-loader and required LAZILY, only when a caller
|
|
33
|
+
// actually asks for bundled policies (see the loader section below).
|
|
34
|
+
// * STRICT: a wrong schemaVersion, an unknown exception type key, a severity
|
|
35
|
+
// not in {info,warning,error}, or a malformed toleranceCents is a NAMED
|
|
36
|
+
// hard error — never a silent no-op or partial accept.
|
|
37
|
+
// * GROUNDED IN reconcile.js: the legal exception type strings and severity
|
|
38
|
+
// values are REUSED from EXCEPTION/SEVERITY, so a typo'd type is a
|
|
39
|
+
// validation error rather than a silently-ignored key.
|
|
40
|
+
|
|
41
|
+
const { EXCEPTION, SEVERITY, compareExceptions } = require("./reconcile");
|
|
42
|
+
|
|
43
|
+
// Bump only on an INCOMPATIBLE schema change. readPolicy rejects anything else.
|
|
44
|
+
const SCHEMA_VERSION = 1;
|
|
45
|
+
|
|
46
|
+
// The set of legal exception type strings, derived from reconcile.js (NOT
|
|
47
|
+
// re-declared here) so the two can never drift. Because it is enum-derived, a
|
|
48
|
+
// new engine exception type (e.g. `ambiguous_deposit`) becomes an accepted
|
|
49
|
+
// `severities`/`citations` key automatically — no re-listing here — so a state
|
|
50
|
+
// can grade it (escalate the default WARNING to a hard ERROR) the day the
|
|
51
|
+
// engine learns to detect it.
|
|
52
|
+
const EXCEPTION_TYPES = Object.freeze(new Set(Object.values(EXCEPTION)));
|
|
53
|
+
// The set of legal severity strings, likewise derived.
|
|
54
|
+
const SEVERITY_VALUES = Object.freeze(new Set(Object.values(SEVERITY)));
|
|
55
|
+
|
|
56
|
+
class PolicyError extends Error {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "PolicyError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// readPolicy(textOrObj) -> validated, frozen policy object
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
//
|
|
67
|
+
// Accepts either a JSON string (e.g. a fixture file's contents) or an already
|
|
68
|
+
// parsed plain object. Parsing and validation are separated so callers can
|
|
69
|
+
// validate an in-memory object without serializing it. PURE: no file I/O here;
|
|
70
|
+
// the caller reads the file and passes the text.
|
|
71
|
+
function readPolicy(input) {
|
|
72
|
+
let obj = input;
|
|
73
|
+
if (typeof input === "string") {
|
|
74
|
+
try {
|
|
75
|
+
obj = JSON.parse(input);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
throw new PolicyError(`policy is not valid JSON: ${e.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return validatePolicy(obj);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// validatePolicy(obj) -> validated, frozen policy object
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
//
|
|
87
|
+
// Strictly validates and returns a NEW frozen, canonical policy object. Throws
|
|
88
|
+
// PolicyError on the first defect found. Never mutates the input.
|
|
89
|
+
function validatePolicy(obj) {
|
|
90
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
91
|
+
throw new PolicyError("policy must be a JSON object");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- schemaVersion: must be present and exactly the supported integer ----
|
|
95
|
+
if (!Object.prototype.hasOwnProperty.call(obj, "schemaVersion")) {
|
|
96
|
+
throw new PolicyError("policy is missing required field: schemaVersion");
|
|
97
|
+
}
|
|
98
|
+
if (obj.schemaVersion !== SCHEMA_VERSION) {
|
|
99
|
+
throw new PolicyError(
|
|
100
|
+
`unsupported policy schemaVersion ${JSON.stringify(
|
|
101
|
+
obj.schemaVersion
|
|
102
|
+
)}; this build understands schemaVersion ${SCHEMA_VERSION}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- state: a non-empty human label --------------------------------------
|
|
107
|
+
if (typeof obj.state !== "string" || obj.state.trim() === "") {
|
|
108
|
+
throw new PolicyError("policy.state must be a non-empty string label");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- severities: type -> severity override map ---------------------------
|
|
112
|
+
if (!Object.prototype.hasOwnProperty.call(obj, "severities")) {
|
|
113
|
+
throw new PolicyError("policy is missing required field: severities");
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
obj.severities === null ||
|
|
117
|
+
typeof obj.severities !== "object" ||
|
|
118
|
+
Array.isArray(obj.severities)
|
|
119
|
+
) {
|
|
120
|
+
throw new PolicyError("policy.severities must be an object map");
|
|
121
|
+
}
|
|
122
|
+
const severities = {};
|
|
123
|
+
for (const key of Object.keys(obj.severities)) {
|
|
124
|
+
if (!EXCEPTION_TYPES.has(key)) {
|
|
125
|
+
throw new PolicyError(
|
|
126
|
+
`policy.severities has unknown exception type ${JSON.stringify(key)}; ` +
|
|
127
|
+
`legal types are: ${[...EXCEPTION_TYPES].sort().join(", ")}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const val = obj.severities[key];
|
|
131
|
+
if (!SEVERITY_VALUES.has(val)) {
|
|
132
|
+
throw new PolicyError(
|
|
133
|
+
`policy.severities[${JSON.stringify(key)}] has invalid severity ` +
|
|
134
|
+
`${JSON.stringify(val)}; must be one of: ` +
|
|
135
|
+
`${[...SEVERITY_VALUES].sort().join(", ")}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
severities[key] = val;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---- citations: optional type -> statute/rule string map -----------------
|
|
142
|
+
// Carried into the report so each severity override is grounded in the rule
|
|
143
|
+
// it rests on. A citation for a type NOT present in severities is rejected:
|
|
144
|
+
// citing a rule you do not actually apply is misleading in an audit.
|
|
145
|
+
const citations = {};
|
|
146
|
+
if (Object.prototype.hasOwnProperty.call(obj, "citations")) {
|
|
147
|
+
if (
|
|
148
|
+
obj.citations === null ||
|
|
149
|
+
typeof obj.citations !== "object" ||
|
|
150
|
+
Array.isArray(obj.citations)
|
|
151
|
+
) {
|
|
152
|
+
throw new PolicyError("policy.citations must be an object map");
|
|
153
|
+
}
|
|
154
|
+
for (const key of Object.keys(obj.citations)) {
|
|
155
|
+
if (!EXCEPTION_TYPES.has(key)) {
|
|
156
|
+
throw new PolicyError(
|
|
157
|
+
`policy.citations has unknown exception type ${JSON.stringify(key)}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (!Object.prototype.hasOwnProperty.call(severities, key)) {
|
|
161
|
+
throw new PolicyError(
|
|
162
|
+
`policy.citations[${JSON.stringify(key)}] cites a rule for a type ` +
|
|
163
|
+
"with no severity override; cite only the overrides you apply"
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const cite = obj.citations[key];
|
|
167
|
+
if (typeof cite !== "string" || cite.trim() === "") {
|
|
168
|
+
throw new PolicyError(
|
|
169
|
+
`policy.citations[${JSON.stringify(key)}] must be a non-empty string`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
citations[key] = cite;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- toleranceCents: optional non-negative integer cents -----------------
|
|
177
|
+
let toleranceCents;
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(obj, "toleranceCents")) {
|
|
179
|
+
const t = obj.toleranceCents;
|
|
180
|
+
if (!Number.isInteger(t) || t < 0) {
|
|
181
|
+
throw new PolicyError(
|
|
182
|
+
`policy.toleranceCents must be a non-negative integer (cents); got ` +
|
|
183
|
+
`${JSON.stringify(t)}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
toleranceCents = t;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Build a canonical, frozen result. Keys are inserted in a fixed order so the
|
|
190
|
+
// object's own enumeration order is deterministic regardless of input order.
|
|
191
|
+
const out = { schemaVersion: SCHEMA_VERSION, state: obj.state };
|
|
192
|
+
out.severities = Object.freeze(sortedMap(severities));
|
|
193
|
+
out.citations = Object.freeze(sortedMap(citations));
|
|
194
|
+
if (toleranceCents !== undefined) out.toleranceCents = toleranceCents;
|
|
195
|
+
return Object.freeze(out);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Return a new object with the same entries in sorted-key order (deterministic
|
|
199
|
+
// enumeration; pure).
|
|
200
|
+
function sortedMap(m) {
|
|
201
|
+
const out = {};
|
|
202
|
+
for (const k of Object.keys(m).sort()) out[k] = m[k];
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// applyPolicy(reconcileResult, policy) -> NEW reconcile-shaped result
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
//
|
|
210
|
+
// Returns a NEW result whose exceptions have their `severity` replaced by the
|
|
211
|
+
// policy override when one is present for that type, and a `citation` attached
|
|
212
|
+
// when the policy supplies one. Records, amounts, labels, details, and balances
|
|
213
|
+
// are left untouched. Deterministic and side-effect-free: the input result is
|
|
214
|
+
// not mutated.
|
|
215
|
+
//
|
|
216
|
+
// RE-SORT AFTER ESCALATION. reconcile.js sorts its exceptions errors-first, and
|
|
217
|
+
// the HTML/CSV renderers (and the human reading the signed packet) rely on that
|
|
218
|
+
// order so an out-of-trust ERROR sits at the top. Because a policy can ESCALATE
|
|
219
|
+
// a warning/info row to ERROR (or de-escalate), the input order is no longer
|
|
220
|
+
// valid for the new severities. We re-apply reconcile's exact stable comparator
|
|
221
|
+
// (compareExceptions, imported — not re-implemented — so the two cannot drift)
|
|
222
|
+
// so a freshly-escalated ERROR re-sorts to the top exactly as a natively
|
|
223
|
+
// detected one would. Order-only; the verdict/counts are order-independent.
|
|
224
|
+
//
|
|
225
|
+
// When `policy` is null/undefined the INPUT is returned UNCHANGED (same object
|
|
226
|
+
// reference) — the no-policy path is byte-for-byte today's DEFAULT_SEVERITY
|
|
227
|
+
// baseline behaviour.
|
|
228
|
+
function applyPolicy(reconcileResult, policy) {
|
|
229
|
+
if (policy === null || policy === undefined) {
|
|
230
|
+
return reconcileResult;
|
|
231
|
+
}
|
|
232
|
+
// Defensive: a caller must hand us a validated policy. Re-validate cheaply by
|
|
233
|
+
// checking the shape we depend on rather than trusting a foreign object.
|
|
234
|
+
if (
|
|
235
|
+
typeof policy !== "object" ||
|
|
236
|
+
policy.severities === null ||
|
|
237
|
+
typeof policy.severities !== "object"
|
|
238
|
+
) {
|
|
239
|
+
throw new PolicyError("applyPolicy requires a validated policy object");
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
reconcileResult === null ||
|
|
243
|
+
typeof reconcileResult !== "object" ||
|
|
244
|
+
!Array.isArray(reconcileResult.exceptions)
|
|
245
|
+
) {
|
|
246
|
+
throw new PolicyError(
|
|
247
|
+
"applyPolicy requires a reconcile result with an exceptions array"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const severities = policy.severities;
|
|
252
|
+
const citations = policy.citations || {};
|
|
253
|
+
|
|
254
|
+
const exceptions = reconcileResult.exceptions.map((ex) => {
|
|
255
|
+
const hasOverride = Object.prototype.hasOwnProperty.call(
|
|
256
|
+
severities,
|
|
257
|
+
ex.type
|
|
258
|
+
);
|
|
259
|
+
const hasCitation = Object.prototype.hasOwnProperty.call(
|
|
260
|
+
citations,
|
|
261
|
+
ex.type
|
|
262
|
+
);
|
|
263
|
+
// No override and no citation: pass the exception through unchanged. We
|
|
264
|
+
// still return a shallow copy so the result is a fresh tree (no aliasing
|
|
265
|
+
// back into the input's array), keeping applyPolicy side-effect-free.
|
|
266
|
+
const next = {
|
|
267
|
+
type: ex.type,
|
|
268
|
+
severity: hasOverride ? severities[ex.type] : ex.severity,
|
|
269
|
+
amount: ex.amount,
|
|
270
|
+
label: ex.label,
|
|
271
|
+
detail: ex.detail,
|
|
272
|
+
records: ex.records,
|
|
273
|
+
};
|
|
274
|
+
if (hasCitation) next.citation = citations[ex.type];
|
|
275
|
+
return next;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Re-sort under the NEW (possibly escalated) severities, using reconcile's
|
|
279
|
+
// own stable comparator. .sort is in-place on our freshly-built array (the
|
|
280
|
+
// input result and its exceptions array are untouched), so this stays pure.
|
|
281
|
+
exceptions.sort(compareExceptions);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
balances: reconcileResult.balances,
|
|
285
|
+
tiesOut: reconcileResult.tiesOut,
|
|
286
|
+
exceptions,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Bundled per-state fixture policies + `--state <code>` resolution.
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
//
|
|
294
|
+
// The product ships a small set of DRAFT / NOT-LEGAL-ADVICE skeleton policies
|
|
295
|
+
// under trustledger/fixtures/policy. `vh trust reconcile --state <code>` lets a
|
|
296
|
+
// broker pick one WITHOUT having to point at a file path, by naming the policy's
|
|
297
|
+
// `state` label (or, equivalently, the fixture filename). This is the ONLY part
|
|
298
|
+
// of this module that touches the filesystem, and it reads only from the
|
|
299
|
+
// package's own bundled fixtures directory — never a caller path — so the result
|
|
300
|
+
// stays deterministic and the rest of the module stays pure.
|
|
301
|
+
//
|
|
302
|
+
// ISOLATED IMPURE SEAM (T-65.1). The actual fs/path calls live in ONE separate
|
|
303
|
+
// module, ./lib/policy-bundled-loader, and that module is required LAZILY
|
|
304
|
+
// inside bundledPolicies() — never at this file's top level. So loading
|
|
305
|
+
// policy.js on the browser path executes no impure require, and a bundler can
|
|
306
|
+
// shim the loader (e.g. inline the fixture JSON) without touching the pure
|
|
307
|
+
// functions above. Validation, sorting, and PolicyError naming all remain HERE,
|
|
308
|
+
// so the loader is raw I/O only and the observable behavior is byte-identical
|
|
309
|
+
// to when the fs calls were inline.
|
|
310
|
+
|
|
311
|
+
// Normalize a state code/label for comparison: lowercase, collapse runs of
|
|
312
|
+
// non-alphanumerics to a single space, trim. So "California", "california",
|
|
313
|
+
// and "CALIFORNIA " all resolve alike, and a verbose label like
|
|
314
|
+
// "EXAMPLE-STATE (illustrative override)" can be addressed by its leading code.
|
|
315
|
+
function normStateCode(s) {
|
|
316
|
+
return String(s == null ? "" : s)
|
|
317
|
+
.toLowerCase()
|
|
318
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
319
|
+
.trim();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// List the bundled fixture policies as { code, file, policy } entries, where
|
|
323
|
+
// `code` is the fixture filename without ".json". Validates each on load so a
|
|
324
|
+
// shipped fixture that drifts out of schema is a hard, named error rather than a
|
|
325
|
+
// silent miss. Deterministic (filenames are sorted).
|
|
326
|
+
function bundledPolicies() {
|
|
327
|
+
let names;
|
|
328
|
+
try {
|
|
329
|
+
// LAZY require of the isolated impure seam: fs/path load only when a
|
|
330
|
+
// caller actually asks for bundled policies, never when policy.js loads.
|
|
331
|
+
names = require("./lib/policy-bundled-loader").listBundledPolicyNames();
|
|
332
|
+
} catch (e) {
|
|
333
|
+
throw new PolicyError(`cannot read bundled policy directory: ${e.message}`);
|
|
334
|
+
}
|
|
335
|
+
return names
|
|
336
|
+
.sort()
|
|
337
|
+
.map((file) => {
|
|
338
|
+
let full;
|
|
339
|
+
let policy;
|
|
340
|
+
try {
|
|
341
|
+
const loaded = require("./lib/policy-bundled-loader").readBundledPolicyFile(file);
|
|
342
|
+
full = loaded.full;
|
|
343
|
+
policy = readPolicy(loaded.text);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
throw new PolicyError(`bundled policy ${file} is invalid: ${e.message}`);
|
|
346
|
+
}
|
|
347
|
+
return { code: file.replace(/\.json$/, ""), file: full, policy };
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Resolve a `--state <code>` to a validated bundled policy. A code matches when
|
|
352
|
+
// it equals (after normalization) EITHER the fixture filename code OR the
|
|
353
|
+
// policy's `state` label. An unknown code is a clear PolicyError that lists the
|
|
354
|
+
// codes that ARE available, so the usage error is actionable.
|
|
355
|
+
function resolveState(code) {
|
|
356
|
+
const want = normStateCode(code);
|
|
357
|
+
if (want === "") {
|
|
358
|
+
throw new PolicyError("--state requires a non-empty state code");
|
|
359
|
+
}
|
|
360
|
+
const all = bundledPolicies();
|
|
361
|
+
for (const entry of all) {
|
|
362
|
+
if (
|
|
363
|
+
normStateCode(entry.code) === want ||
|
|
364
|
+
normStateCode(entry.policy.state) === want
|
|
365
|
+
) {
|
|
366
|
+
return entry.policy;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const codes = all.map((e) => e.code).sort().join(", ");
|
|
370
|
+
throw new PolicyError(
|
|
371
|
+
`unknown --state "${code}"; bundled states are: ${codes}`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = {
|
|
376
|
+
SCHEMA_VERSION,
|
|
377
|
+
PolicyError,
|
|
378
|
+
readPolicy,
|
|
379
|
+
validatePolicy,
|
|
380
|
+
applyPolicy,
|
|
381
|
+
// bundled per-state fixtures + --state resolution
|
|
382
|
+
bundledPolicies,
|
|
383
|
+
resolveState,
|
|
384
|
+
normStateCode,
|
|
385
|
+
// exported for focused tests / reuse
|
|
386
|
+
EXCEPTION_TYPES,
|
|
387
|
+
SEVERITY_VALUES,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// BUNDLED_DIR stays on the export surface (same value as always) but resolves
|
|
391
|
+
// LAZILY through the isolated loader, so exporting it does not drag fs/path
|
|
392
|
+
// into the browser path at load time.
|
|
393
|
+
Object.defineProperty(module.exports, "BUNDLED_DIR", {
|
|
394
|
+
enumerable: true,
|
|
395
|
+
get() {
|
|
396
|
+
return require("./lib/policy-bundled-loader").BUNDLED_DIR;
|
|
397
|
+
},
|
|
398
|
+
});
|