@atcn/subledger 1.3.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/dist/verify.js ADDED
@@ -0,0 +1,372 @@
1
+ import { verifyClosurePackage } from "@atcn/core";
2
+ import { ClosurePackageSchema, digestOf, verifyPayload } from "@atcn/schema";
3
+ import { CLEARING_SOURCE, clearingFacts, settlementEvidence, undoneBatch } from "./bridge.js";
4
+ import { CLOSURE_DOCUMENT_TYPE, RECEIPT_DOCUMENT_TYPE, SignedClosureSchema, SignedReceiptSchema, SIGNED_BY_HOSTED_SERVICE, SUBLEDGER_VERIFIER_VERSION, SUPPORTED_SUBLEDGER_SCHEMA_VERSIONS, } from "./documents.js";
5
+ import { receiptTotals, rollupFor } from "./projection.js";
6
+ import { verifyCountersignature, verifyStatementSignature } from "./response.js";
7
+ import { ATTESTABLE_FIELDS } from "./types.js";
8
+ const SERVICE_ACTOR = "svc_atcn";
9
+ function check(name, problems) {
10
+ return { name, ok: problems.length === 0, details: problems };
11
+ }
12
+ function signatureCheck(doc, at, trustedKeys) {
13
+ const key = trustedKeys.find((k) => k.key_id === doc.signature.key_id && k.key_version === doc.signature.key_version && k.actor_id === SERVICE_ACTOR);
14
+ if (!key)
15
+ return check("issuer_signature", [`signing key ${doc.signature.key_id}#${doc.signature.key_version} is not a trusted service key`]);
16
+ const problems = [];
17
+ if (!(key.valid_from <= at && (key.revoked_at === null || key.revoked_at > at)))
18
+ problems.push("signing key was not valid at signing time");
19
+ if (!verifyPayload(doc, key.public_key))
20
+ problems.push("signature does not verify over the canonical payload");
21
+ return check("issuer_signature", problems);
22
+ }
23
+ function operatorSignatureCheck(payload, operatorId, signatures, options) {
24
+ const list = signatures ?? [];
25
+ const required = options.requireOperatorSignature === true;
26
+ if (list.length === 0)
27
+ return required ? check("operator_signatures", ["no countersignature by the operator's own key"]) : { name: "operator_signatures", ok: true, details: ["no operator countersignature"] };
28
+ if (!options.operatorKeys) {
29
+ const note = `operator keys not supplied; ${list.length} countersignature(s) not checked`;
30
+ return required ? check("operator_signatures", [note]) : { name: "operator_signatures", ok: true, details: [note] };
31
+ }
32
+ const problems = [];
33
+ for (const sig of list) {
34
+ const key = options.operatorKeys.find((k) => k.operator_id === operatorId && k.key_id === sig.key_id);
35
+ if (!key)
36
+ problems.push(`key ${sig.key_id} is not a published key of operator ${operatorId}`);
37
+ else if (key.created_at > sig.signed_at || (key.revoked_at !== null && key.revoked_at <= sig.signed_at))
38
+ problems.push(`key ${sig.key_id} was not valid when the countersignature was recorded`);
39
+ else if (!verifyCountersignature(payload, sig.value, key.public_key))
40
+ problems.push(`countersignature by ${sig.key_id} does not verify over the canonical payload`);
41
+ }
42
+ return check("operator_signatures", problems);
43
+ }
44
+ /** Detects the document type and verifies it offline: no network calls, no service access (acceptance 9). */
45
+ export function verifySubledgerDocument(input, options) {
46
+ const documentType = input?.payload?.document_type;
47
+ if (documentType === RECEIPT_DOCUMENT_TYPE)
48
+ return verifyReceipt(input, options);
49
+ if (documentType === CLOSURE_DOCUMENT_TYPE)
50
+ return verifyClosure(input, options);
51
+ return { valid: false, document_type: null, checks: [check("document_type", [`unknown document_type ${String(documentType)}`])] };
52
+ }
53
+ function finish(documentType, checks) {
54
+ return { valid: checks.every((c) => c.ok), document_type: documentType, checks };
55
+ }
56
+ function schemaFailure(documentType, issues) {
57
+ return finish(documentType, [check("schema", issues.map((i) => `${i.path.map(String).join(".")}: ${i.message}`))]);
58
+ }
59
+ /**
60
+ * Runs before schema parsing. Without it, a document from a newer schema fails with a misleading
61
+ * schema error, or with a signature mismatch once unknown fields are stripped.
62
+ */
63
+ function unsupportedVersion(documentType, input) {
64
+ const version = input?.payload?.schema_version;
65
+ const supported = SUPPORTED_SUBLEDGER_SCHEMA_VERSIONS;
66
+ if (supported.includes(version))
67
+ return null;
68
+ const detail = `unsupported schema_version ${String(version)}: this verifier (@atcn/subledger ${SUBLEDGER_VERIFIER_VERSION}) supports ` +
69
+ `${SUPPORTED_SUBLEDGER_SCHEMA_VERSIONS.join(" and ")}. Upgrade @atcn/verify-cli (or @atcn/subledger) to the minimum ` +
70
+ "version listed for this schema in packages/schema/COMPATIBILITY.md.";
71
+ return { valid: false, document_type: documentType, unsupported_schema_version: String(version), checks: [check("schema_version", [detail])] };
72
+ }
73
+ const SCHEMA_1_2_ASSERTERS = ["buyer", "provider"];
74
+ /** A document that declares 1.2 must not carry 1.3 fields, which 1.2 verifiers cannot read. */
75
+ function versionFeatureProblems(version, signedBy, claims, hasObligationLinks) {
76
+ if (version !== "1.2")
77
+ return [];
78
+ const problems = claims.filter((c) => !SCHEMA_1_2_ASSERTERS.includes(c.asserted_by)).map((c) => `schema 1.2 does not allow asserted_by ${c.asserted_by}`);
79
+ if (claims.some((c) => c.assurance.includes("network_recorded")))
80
+ problems.push("schema 1.2 does not allow assurance network_recorded");
81
+ if (hasObligationLinks)
82
+ problems.push("schema 1.2 does not allow obligation_links");
83
+ if (signedBy !== SIGNED_BY_HOSTED_SERVICE)
84
+ problems.push(`schema 1.2 does not allow signed_by ${signedBy}`);
85
+ return problems;
86
+ }
87
+ function reversalProblems(events) {
88
+ const byId = new Map(events.map((e) => [e.financial_event_id, e]));
89
+ const problems = [];
90
+ for (const e of events.filter((x) => x.type === "reversal")) {
91
+ const original = e.reverses_event_id ? byId.get(e.reverses_event_id) : undefined;
92
+ if (!original)
93
+ problems.push(`reversal ${e.financial_event_id} references an event not in the document`);
94
+ else if (original.amount_minor !== e.amount_minor || original.currency !== e.currency)
95
+ problems.push(`reversal ${e.financial_event_id} does not match the reversed amount and currency`);
96
+ }
97
+ return problems;
98
+ }
99
+ export function verifyReceipt(input, options) {
100
+ const unsupported = unsupportedVersion(RECEIPT_DOCUMENT_TYPE, input);
101
+ if (unsupported)
102
+ return unsupported;
103
+ const parsed = SignedReceiptSchema.safeParse(input);
104
+ if (!parsed.success)
105
+ return schemaFailure(RECEIPT_DOCUMENT_TYPE, parsed.error.issues);
106
+ const doc = parsed.data;
107
+ const r = doc.payload;
108
+ const schemaProblems = versionFeatureProblems(r.schema_version, r.issuer.signed_by, r.delivery_claims, false);
109
+ const checks = [check("schema", schemaProblems), signatureCheck(doc, r.issued_at, options.trustedKeys), operatorSignatureCheck(r, r.issuer.operator_id, doc.operator_signatures, options)];
110
+ const events = r.financial_events.map(({ allocation_version: _version, ...e }) => ({ ...e, liability_owner: null, economic_event_id: null, fx: null }));
111
+ checks.push(check("reversals", reversalProblems(events)));
112
+ const recomputed = receiptTotals(r.delegation, events);
113
+ checks.push(check("totals", digestOf(recomputed) === digestOf(r.totals) ? [] : ["totals do not match the listed financial events"]));
114
+ const fieldProblems = [];
115
+ const expectedUnverified = ATTESTABLE_FIELDS.filter((f) => r.field_status[f] !== "missing");
116
+ if (digestOf(expectedUnverified) !== digestOf(r.unverified_fields))
117
+ fieldProblems.push("unverified_fields must list every non-missing field");
118
+ for (const f of ATTESTABLE_FIELDS)
119
+ if (!r.field_status[f])
120
+ fieldProblems.push(`field_status is missing ${f}`);
121
+ checks.push(check("field_disclosure", fieldProblems));
122
+ checks.push(receiptChainCheck(r, options));
123
+ return finish(RECEIPT_DOCUMENT_TYPE, checks);
124
+ }
125
+ function receiptChainCheck(r, options) {
126
+ if (r.revision === 1)
127
+ return check("revision_chain", r.previous_receipt_digest === null && r.previous_receipt_id === null ? [] : ["revision 1 must not reference a previous receipt"]);
128
+ if (r.previous_receipt_digest === null || r.previous_receipt_id === null)
129
+ return check("revision_chain", [`revision ${r.revision} must reference its previous receipt`]);
130
+ if (options.previous === undefined)
131
+ return { name: "revision_chain", ok: true, details: ["previous revision not supplied; link digest not checked"] };
132
+ const previous = SignedReceiptSchema.safeParse(options.previous);
133
+ if (!previous.success)
134
+ return check("revision_chain", ["supplied previous receipt is not a valid receipt"]);
135
+ const p = previous.data.payload;
136
+ const problems = [];
137
+ if (p.receipt_id !== r.previous_receipt_id)
138
+ problems.push("previous_receipt_id does not match the supplied receipt");
139
+ if (digestOf(p) !== r.previous_receipt_digest)
140
+ problems.push("previous_receipt_digest does not match the supplied receipt");
141
+ if (p.revision + 1 !== r.revision)
142
+ problems.push("revision is not the next after the supplied receipt");
143
+ if (p.delegation.delegation_id !== r.delegation.delegation_id)
144
+ problems.push("previous receipt covers a different delegation");
145
+ if (!signatureCheck(previous.data, p.issued_at, options.trustedKeys).ok)
146
+ problems.push("previous receipt signature does not verify");
147
+ return check("revision_chain", problems);
148
+ }
149
+ export function verifyClosure(input, options) {
150
+ const unsupported = unsupportedVersion(CLOSURE_DOCUMENT_TYPE, input);
151
+ if (unsupported)
152
+ return unsupported;
153
+ const parsed = SignedClosureSchema.safeParse(input);
154
+ if (!parsed.success)
155
+ return schemaFailure(CLOSURE_DOCUMENT_TYPE, parsed.error.issues);
156
+ const doc = parsed.data;
157
+ const c = doc.payload;
158
+ const schemaProblems = versionFeatureProblems(c.schema_version, c.issuer.signed_by, c.delivery_claims, c.obligation_links !== undefined);
159
+ const checks = [check("schema", schemaProblems), signatureCheck(doc, c.generated_at, options.trustedKeys), operatorSignatureCheck(c, c.issuer.operator_id, doc.operator_signatures, options)];
160
+ const digestProblems = c.financial_events.filter((e) => digestOf(e.record) !== e.event_digest).map((e) => `event ${e.record.financial_event_id} digest mismatch`);
161
+ const ids = c.financial_events.map((e) => e.record.financial_event_id);
162
+ if (new Set(ids).size !== ids.length)
163
+ digestProblems.push("a financial event appears more than once");
164
+ checks.push(check("event_digests", digestProblems));
165
+ checks.push(check("lineage", lineageProblems(c)));
166
+ checks.push(check("reversals", reversalProblems(c.financial_events.map((e) => e.record))));
167
+ checks.push(check("allocations", allocationProblems(c)));
168
+ const recomputed = rollupFor(c.task, c.delegations, c.financial_events, c.allocations);
169
+ checks.push(check("totals", digestOf(recomputed) === digestOf(c.rollup) ? [] : ["roll-up does not match events, attribution, and allocations"]));
170
+ checks.push(check("provider_responses", responseProblems(c)));
171
+ checks.push(closureChainCheck(c, options));
172
+ checks.push(obligationLinkCheck(c, options));
173
+ return finish(CLOSURE_DOCUMENT_TYPE, checks);
174
+ }
175
+ /**
176
+ * Delegations backed by clearing-network obligations. Every event recorded from the clearing journal must sit on a
177
+ * linked delegation. With the obligations' closure packages, each package must verify, contain the linked decision,
178
+ * and its journal batches posted before the closure must produce exactly the recorded events.
179
+ */
180
+ function obligationLinkCheck(c, options) {
181
+ const name = "obligation_links";
182
+ const links = c.obligation_links ?? [];
183
+ const problems = [];
184
+ const delegationIds = new Set(c.delegations.map((d) => d.delegation_id));
185
+ for (const link of links)
186
+ if (!delegationIds.has(link.delegation_id))
187
+ problems.push(`linked delegation ${link.delegation_id} is not in the task`);
188
+ if (new Set(links.map((l) => l.obligation_id)).size !== links.length)
189
+ problems.push("an obligation is linked more than once");
190
+ const linked = new Set(links.map((l) => l.delegation_id));
191
+ for (const e of c.financial_events) {
192
+ if (e.record.source === CLEARING_SOURCE && !linked.has(e.attributed_to))
193
+ problems.push(`clearing-network event ${e.record.financial_event_id} is attributed to a node without a linked obligation`);
194
+ }
195
+ if (problems.length > 0)
196
+ return check(name, problems);
197
+ if (links.length === 0)
198
+ return { name, ok: true, details: ["no delegation is backed by an obligation"] };
199
+ if (!options.obligationPackages)
200
+ return { name, ok: true, details: [`${links.length} linked obligation(s); closure packages not supplied; not cross-checked`] };
201
+ for (const link of links)
202
+ problems.push(...linkProblems(c, link, options.obligationPackages, options.trustedKeys));
203
+ return problems.length > 0 ? check(name, problems) : { name, ok: true, details: [`${links.length} linked obligation(s) match their closure packages`] };
204
+ }
205
+ function linkProblems(c, link, packages, trustedKeys) {
206
+ const pkg = packages
207
+ .map((p) => ClosurePackageSchema.safeParse(p))
208
+ .find((r) => r.success && r.data.payload.obligations.some((o) => o.obligation_id === link.obligation_id && !o.redacted));
209
+ if (!pkg?.success)
210
+ return [`no supplied closure package covers obligation ${link.obligation_id}`];
211
+ const body = pkg.data.payload;
212
+ const problems = [];
213
+ const report = verifyClosurePackage(pkg.data, { trustedKeys });
214
+ if (!report.valid)
215
+ problems.push(`closure package for ${link.obligation_id} does not verify (${report.checks.filter((x) => !x.ok).map((x) => x.name).join(", ")})`);
216
+ if (link.decision_id !== null) {
217
+ const decision = body.decisions.find((d) => d.decision_id === link.decision_id && d.obligation_id === link.obligation_id);
218
+ if (!decision)
219
+ problems.push(`decision ${link.decision_id} is not in the package for ${link.obligation_id}`);
220
+ else if (decision.decision_digest !== link.decision_digest)
221
+ problems.push(`decision ${link.decision_id} digest differs from the package`);
222
+ }
223
+ const batches = body.posting_batches.filter((b) => b.obligation_id === link.obligation_id);
224
+ const expected = batches
225
+ .filter((b) => b.posted_at <= c.generated_at)
226
+ .flatMap((b) => clearingFacts(b, undoneBatch(b, batches, body.settlement_events)).map((f) => ({ ...f, evidence: settlementEvidence(b, body.settlement_events) })));
227
+ const recorded = c.financial_events.filter((e) => e.record.source === CLEARING_SOURCE && e.attributed_to === link.delegation_id).map((e) => e.record);
228
+ const byKey = new Map(recorded.map((e) => [e.source_event_id, e]));
229
+ for (const f of expected) {
230
+ const e = byKey.get(f.key);
231
+ if (!e) {
232
+ problems.push(`journal fact ${f.key} (${f.type} ${f.amount_minor} ${f.currency}) is missing from the closure`);
233
+ continue;
234
+ }
235
+ if (e.type !== f.type || e.amount_minor !== f.amount_minor || e.currency !== f.currency)
236
+ problems.push(`event ${e.financial_event_id} does not match journal fact ${f.key}`);
237
+ if (digestOf(e.evidence) !== digestOf(f.evidence))
238
+ problems.push(`event ${e.financial_event_id} does not cite the settlement report and basis in the package`);
239
+ if (f.reverses_key !== null && byKey.get(f.reverses_key)?.financial_event_id !== e.reverses_event_id)
240
+ problems.push(`reversal ${e.financial_event_id} does not undo the event for ${f.reverses_key}`);
241
+ }
242
+ const expectedKeys = new Set(expected.map((f) => f.key));
243
+ for (const e of recorded)
244
+ if (!expectedKeys.has(e.source_event_id))
245
+ problems.push(`event ${e.financial_event_id} (${e.source_event_id}) has no matching fact in the package's journal`);
246
+ return problems;
247
+ }
248
+ function lineageProblems(c) {
249
+ const problems = [];
250
+ const byId = new Map(c.delegations.map((d) => [d.delegation_id, d]));
251
+ if (byId.size !== c.delegations.length)
252
+ problems.push("duplicate delegation IDs");
253
+ for (const d of c.delegations) {
254
+ if (d.parent_delegation_id === null) {
255
+ if (d.depth !== 1)
256
+ problems.push(`delegation ${d.delegation_id} is a direct child of the task but has depth ${d.depth}`);
257
+ continue;
258
+ }
259
+ const parent = byId.get(d.parent_delegation_id);
260
+ if (!parent)
261
+ problems.push(`delegation ${d.delegation_id} references a parent outside the task`);
262
+ else if (parent.depth + 1 !== d.depth)
263
+ problems.push(`delegation ${d.delegation_id} depth does not follow its parent`);
264
+ const seen = new Set([d.delegation_id]);
265
+ let current = parent;
266
+ while (current) {
267
+ if (seen.has(current.delegation_id)) {
268
+ problems.push(`cycle through ${d.delegation_id}`);
269
+ break;
270
+ }
271
+ seen.add(current.delegation_id);
272
+ current = current.parent_delegation_id ? byId.get(current.parent_delegation_id) : undefined;
273
+ }
274
+ }
275
+ const nodes = new Set([c.task.task_id, ...byId.keys()]);
276
+ for (const e of c.financial_events)
277
+ if (!nodes.has(e.attributed_to))
278
+ problems.push(`event ${e.record.financial_event_id} is attributed outside the task tree`);
279
+ for (const claim of c.delivery_claims)
280
+ if (!byId.has(claim.delegation_id))
281
+ problems.push(`delivery claim ${claim.event_id} references a delegation outside the task`);
282
+ return problems;
283
+ }
284
+ function allocationProblems(c) {
285
+ const problems = [];
286
+ const events = new Map(c.financial_events.map((e) => [e.record.financial_event_id, e]));
287
+ const versions = new Map();
288
+ for (const a of c.allocations) {
289
+ const event = events.get(a.financial_event_id);
290
+ if (!event) {
291
+ problems.push(`allocation ${a.allocation_id} references an event not in the closure`);
292
+ continue;
293
+ }
294
+ if (a.source_event_digest !== event.event_digest)
295
+ problems.push(`allocation ${a.allocation_id} source digest does not match the event`);
296
+ if (a.currency !== event.record.currency)
297
+ problems.push(`allocation ${a.allocation_id} currency differs from its source`);
298
+ if (a.source_amount_minor !== Math.abs(event.record.amount_minor))
299
+ problems.push(`allocation ${a.allocation_id} source amount differs from its event`);
300
+ const sum = a.lines.reduce((total, line) => total + line.amount_minor, 0);
301
+ if (sum !== a.source_amount_minor)
302
+ problems.push(`allocation ${a.allocation_id} lines sum to ${sum}, source is ${a.source_amount_minor}`);
303
+ versions.set(a.financial_event_id, [...(versions.get(a.financial_event_id) ?? []), a.version]);
304
+ }
305
+ for (const [eventId, list] of versions) {
306
+ const sorted = [...list].sort((x, y) => x - y);
307
+ if (sorted.some((v, i) => v !== i + 1))
308
+ problems.push(`allocation versions for ${eventId} are not contiguous from 1`);
309
+ }
310
+ return problems;
311
+ }
312
+ function responseProblems(c) {
313
+ const problems = [];
314
+ const receipts = new Map(c.receipts.map((r) => [r.receipt_id, r]));
315
+ const delegationIds = new Set(c.delegations.map((d) => d.delegation_id));
316
+ for (const r of c.receipts)
317
+ if (!delegationIds.has(r.delegation_id))
318
+ problems.push(`receipt ${r.receipt_id} covers a delegation outside the task`);
319
+ for (const response of c.responses) {
320
+ const receipt = receipts.get(response.receipt_id);
321
+ const s = response.statement;
322
+ if (digestOf(s) !== response.statement_digest)
323
+ problems.push(`response ${response.response_id} statement digest mismatch`);
324
+ if (!receipt) {
325
+ problems.push(`response ${response.response_id} references an unknown receipt`);
326
+ continue;
327
+ }
328
+ if (s.receipt_id !== receipt.receipt_id || s.receipt_digest !== receipt.digest || s.receipt_revision !== receipt.revision || s.receipt_revision !== response.receipt_revision) {
329
+ problems.push(`response ${response.response_id} is not bound to receipt ${receipt.receipt_id} revision ${receipt.revision}`);
330
+ }
331
+ if (s.issuer_operator_id !== c.issuer.operator_id)
332
+ problems.push(`response ${response.response_id} names another issuer`);
333
+ const claimsKeySigned = response.assurance.includes("provider_key_signed");
334
+ if (!claimsKeySigned)
335
+ continue;
336
+ const sig = response.provider_signature;
337
+ const binding = sig ? c.key_bindings.find((b) => b.binding_id === sig.binding_id && b.key_id === sig.key_id) : undefined;
338
+ if (!sig || !binding)
339
+ problems.push(`response ${response.response_id} claims provider_key_signed without a listed key binding`);
340
+ else if (binding.provider_id !== response.provider_id)
341
+ problems.push(`response ${response.response_id} key binding belongs to another provider`);
342
+ else if (binding.created_at > response.created_at || (binding.revoked_at !== null && binding.revoked_at <= response.created_at))
343
+ problems.push(`response ${response.response_id} signed outside the key binding's validity`);
344
+ else if (!verifyStatementSignature(s, sig.value, binding.public_key))
345
+ problems.push(`response ${response.response_id} provider signature does not verify`);
346
+ }
347
+ return problems;
348
+ }
349
+ function closureChainCheck(c, options) {
350
+ if (c.version === 1)
351
+ return check("version_chain", c.previous_closure_digest === null && c.previous_closure_id === null ? [] : ["version 1 must not reference a previous closure"]);
352
+ if (c.previous_closure_digest === null || c.previous_closure_id === null)
353
+ return check("version_chain", [`version ${c.version} must reference its previous closure`]);
354
+ if (options.previous === undefined)
355
+ return { name: "version_chain", ok: true, details: ["previous version not supplied; link digest not checked"] };
356
+ const previous = SignedClosureSchema.safeParse(options.previous);
357
+ if (!previous.success)
358
+ return check("version_chain", ["supplied previous closure is not a valid closure"]);
359
+ const p = previous.data.payload;
360
+ const problems = [];
361
+ if (p.closure_id !== c.previous_closure_id)
362
+ problems.push("previous_closure_id does not match the supplied closure");
363
+ if (digestOf(p) !== c.previous_closure_digest)
364
+ problems.push("previous_closure_digest does not match the supplied closure");
365
+ if (p.version + 1 !== c.version)
366
+ problems.push("version is not the next after the supplied closure");
367
+ if (p.task.task_id !== c.task.task_id)
368
+ problems.push("previous closure covers a different task");
369
+ if (!signatureCheck(previous.data, p.generated_at, options.trustedKeys).ok)
370
+ problems.push("previous closure signature does not verify");
371
+ return check("version_chain", problems);
372
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@atcn/subledger",
3
+ "version": "1.3.0",
4
+ "description": "Agent Work Subledger: deterministic attribution, roll-up, exceptions, signed receipts and closures, and offline verification",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/fadnisnikhil/atcn.git",
9
+ "directory": "packages/subledger"
10
+ },
11
+ "homepage": "https://github.com/fadnisnikhil/atcn/tree/main/packages/subledger#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/fadnisnikhil/atcn/issues"
14
+ },
15
+ "type": "module",
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "atcn-source": "./src/index.ts",
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "test-vectors"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.build.json"
34
+ },
35
+ "dependencies": {
36
+ "@atcn/core": "1.3.0",
37
+ "@atcn/schema": "1.3.0",
38
+ "zod": "^4.1.0"
39
+ }
40
+ }
@@ -0,0 +1,108 @@
1
+ {
2
+ "description": "Subledger response statements and operator countersignatures. Every SDK must reproduce statement, canonical, digest, and signature exactly.",
3
+ "private_key": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA",
4
+ "public_key": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ",
5
+ "statements": [
6
+ {
7
+ "input": {
8
+ "receipt": {
9
+ "receipt_id": "rcp_01J00000000000000000000001",
10
+ "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
11
+ "revision": 2,
12
+ "issuer_operator_id": "ten_01J00000000000000000000001"
13
+ },
14
+ "response_type": "signed_attestation",
15
+ "fields": [
16
+ "financial.amounts",
17
+ "delivery.status",
18
+ "delivery.status"
19
+ ]
20
+ },
21
+ "statement": {
22
+ "document_type": "atcn.subledger.receipt_response",
23
+ "receipt_id": "rcp_01J00000000000000000000001",
24
+ "receipt_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
25
+ "receipt_revision": 2,
26
+ "issuer_operator_id": "ten_01J00000000000000000000001",
27
+ "response_type": "signed_attestation",
28
+ "fields": [
29
+ "delivery.status",
30
+ "financial.amounts"
31
+ ],
32
+ "note": null,
33
+ "evidence": [],
34
+ "corrections": []
35
+ },
36
+ "canonical": "{\"corrections\":[],\"document_type\":\"atcn.subledger.receipt_response\",\"evidence\":[],\"fields\":[\"delivery.status\",\"financial.amounts\"],\"issuer_operator_id\":\"ten_01J00000000000000000000001\",\"note\":null,\"receipt_digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"receipt_id\":\"rcp_01J00000000000000000000001\",\"receipt_revision\":2,\"response_type\":\"signed_attestation\"}",
37
+ "digest": "sha256:0093038c07f3312d67ef6f776bc79234f04e9d781959206ec8ec91386faaed91",
38
+ "signature": "tnGYFB79tOOusw1cJLpBtIcnB6a53fAvsH31g6ikdbrl2o_flEqSWpQQ4QpGg-DzlKtXs5iYzfQe9V-u9UkBCA"
39
+ },
40
+ {
41
+ "input": {
42
+ "receipt": {
43
+ "receipt_id": "rcp_01J00000000000000000000002",
44
+ "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
45
+ "revision": 1,
46
+ "issuer_operator_id": "ten_01J00000000000000000000001"
47
+ },
48
+ "response_type": "propose_correction",
49
+ "fields": [],
50
+ "note": "café: two of three files",
51
+ "evidence": [
52
+ {
53
+ "uri": "https://provider.example/delivery/7",
54
+ "digest": null,
55
+ "evidence_type": "delivery_log"
56
+ }
57
+ ],
58
+ "corrections": [
59
+ {
60
+ "field": "delivery.status",
61
+ "proposed_value": "partial_completion",
62
+ "reason": "file 3 missing"
63
+ }
64
+ ]
65
+ },
66
+ "statement": {
67
+ "document_type": "atcn.subledger.receipt_response",
68
+ "receipt_id": "rcp_01J00000000000000000000002",
69
+ "receipt_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
70
+ "receipt_revision": 1,
71
+ "issuer_operator_id": "ten_01J00000000000000000000001",
72
+ "response_type": "propose_correction",
73
+ "fields": [],
74
+ "note": "café: two of three files",
75
+ "evidence": [
76
+ {
77
+ "uri": "https://provider.example/delivery/7",
78
+ "digest": null,
79
+ "evidence_type": "delivery_log"
80
+ }
81
+ ],
82
+ "corrections": [
83
+ {
84
+ "field": "delivery.status",
85
+ "proposed_value": "partial_completion",
86
+ "reason": "file 3 missing"
87
+ }
88
+ ]
89
+ },
90
+ "canonical": "{\"corrections\":[{\"field\":\"delivery.status\",\"proposed_value\":\"partial_completion\",\"reason\":\"file 3 missing\"}],\"document_type\":\"atcn.subledger.receipt_response\",\"evidence\":[{\"digest\":null,\"evidence_type\":\"delivery_log\",\"uri\":\"https://provider.example/delivery/7\"}],\"fields\":[],\"issuer_operator_id\":\"ten_01J00000000000000000000001\",\"note\":\"café: two of three files\",\"receipt_digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"receipt_id\":\"rcp_01J00000000000000000000002\",\"receipt_revision\":1,\"response_type\":\"propose_correction\"}",
91
+ "digest": "sha256:633530c26ed9133457be0a7d4a1417f24c73dc17fcbd20f58901cfb462390e15",
92
+ "signature": "XKtJa41QABDVCDvyKIGqNejsQAvqHaKqh_yanIrNWPcehrnLCNW28L621ar__eHdqP2-64yvZnwbuJD0efi4Aw"
93
+ }
94
+ ],
95
+ "countersignature": {
96
+ "payload": {
97
+ "document_type": "atcn.subledger.closure",
98
+ "closure_id": "cls_01J00000000000000000000001",
99
+ "version": 1,
100
+ "totals": {
101
+ "USD": {
102
+ "net_cost": 1500
103
+ }
104
+ }
105
+ },
106
+ "signature": "jUnhhTUE28I9wdyO15cFLm1aB9Lo-3r6d1cif7HPFLDIvLr8GngoKNCYtlTftNwxP1b7ARBUkwLga-PywmFtBA"
107
+ }
108
+ }