@atcn/subledger 1.4.0 → 1.5.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/README.md +11 -6
- package/dist/documents.d.ts +1067 -26
- package/dist/documents.js +121 -7
- package/dist/exceptions.d.ts +47 -2
- package/dist/exceptions.js +173 -3
- package/dist/expectations.d.ts +96 -0
- package/dist/expectations.js +236 -0
- package/dist/importing.d.ts +53 -0
- package/dist/importing.js +192 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/outcome.d.ts +36 -0
- package/dist/outcome.js +64 -0
- package/dist/projection.d.ts +28 -3
- package/dist/projection.js +69 -4
- package/dist/rails.d.ts +108 -0
- package/dist/rails.js +242 -0
- package/dist/response.d.ts +2 -0
- package/dist/response.js +1 -0
- package/dist/rollup.d.ts +1 -1
- package/dist/rollup.js +10 -4
- package/dist/types.d.ts +279 -7
- package/dist/types.js +179 -6
- package/dist/usage.d.ts +41 -0
- package/dist/usage.js +86 -0
- package/dist/verify.d.ts +8 -1
- package/dist/verify.js +385 -7
- package/package.json +6 -4
- package/test-vectors/reconciliation-results.json +151 -0
- package/test-vectors/reconciliation.json +107 -0
- package/test-vectors/vectors.json +25666 -1
package/dist/types.js
CHANGED
|
@@ -1,15 +1,64 @@
|
|
|
1
|
-
import { ExecutionDescriptorSchema } from "@atcn/schema";
|
|
1
|
+
import { ExecutionDescriptorSchema, PricingSchema, RefundTermsSchema, SkillRefSchema, UsageSummarySchema } from "@atcn/schema";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
/** Agent Work Subledger record shapes (PRD v1.2 §6-§7). Amounts are integer minor units; currencies are ISO 4217. */
|
|
4
4
|
export const Currency = z.string().regex(/^[A-Z]{3}$/, "ISO 4217 currency code");
|
|
5
5
|
export const Minor = z.number().int().refine(Number.isSafeInteger, "amount must be a safe integer");
|
|
6
6
|
const Iso = z.iso.datetime();
|
|
7
7
|
const Digest = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/** "estimate" and "hold" (schema 1.5) record what was expected before work ran; they are never costs. */
|
|
9
|
+
export const FINANCIAL_EVENT_TYPES = ["quote", "invoice", "charge", "payment_reported", "refund", "reversal", "fee", "credit", "adjustment", "fx_rate", "estimate", "hold"];
|
|
10
|
+
/**
|
|
11
|
+
* Normalized status vocabulary; the provider's original status is always kept beside it. "pending_finality" (schema
|
|
12
|
+
* 1.5) is a payment the rail reports as settled that can still be reversed until the rail's finality window ends.
|
|
13
|
+
*/
|
|
14
|
+
export const NORMALIZED_STATUSES = ["quoted", "issued", "pending", "reported_paid", "pending_finality", "failed", "refunded", "reversed", "void", "unknown"];
|
|
15
|
+
/** Financial event types that bill for work and so may name the skill billed (schema 1.5). */
|
|
16
|
+
export const SKILL_BILLED_EVENT_TYPES = ["quote", "invoice", "charge"];
|
|
17
|
+
/**
|
|
18
|
+
* Estimates and holds (schema 1.5): what the agent, a budget gateway or the operator expected a piece of work to cost
|
|
19
|
+
* before it ran. ATCN records them and reports the gap against actual cost; it never enforces, blocks or reserves.
|
|
20
|
+
*/
|
|
21
|
+
export const EXPECTATION_EVENT_TYPES = ["estimate", "hold"];
|
|
22
|
+
export const EXPECTATION_ISSUERS = ["agent", "gateway", "operator"];
|
|
23
|
+
export const HOLD_STATUSES = ["open", "captured", "released", "expired"];
|
|
24
|
+
/**
|
|
25
|
+
* A signature by a key bound to a registered provider (an agent's provider or a gateway), over the statement of an
|
|
26
|
+
* estimate, a hold, or a terminal delivery claim (schema 1.5).
|
|
27
|
+
*/
|
|
28
|
+
export const KeySignerSchema = z.strictObject({
|
|
29
|
+
provider_id: z.string().min(1),
|
|
30
|
+
binding_id: z.string().min(1),
|
|
31
|
+
key_id: z.string().min(1),
|
|
32
|
+
value: z.string().min(1),
|
|
33
|
+
});
|
|
34
|
+
export const ExpectationSchema = z.strictObject({
|
|
35
|
+
/** Who made the estimate or hold. */
|
|
36
|
+
issued_by: z.enum(EXPECTATION_ISSUERS),
|
|
37
|
+
/** The issuer's own reference, such as a gateway request ID. */
|
|
38
|
+
source_ref: z.string().max(200).nullable(),
|
|
39
|
+
/** How the amount was worked out, for example "max tokens x rate". */
|
|
40
|
+
basis: z.string().max(500).nullable(),
|
|
41
|
+
expires_at: Iso.nullable(),
|
|
42
|
+
/** The source_event_id (same source) of the earlier estimate, or earlier record of the same hold, this one replaces. */
|
|
43
|
+
supersedes: z.string().max(200).nullable(),
|
|
44
|
+
/** Holds only: the hold's status as of this record. */
|
|
45
|
+
hold_status: z.enum(HOLD_STATUSES).optional(),
|
|
46
|
+
/** Absent when the operator recorded it; the record is then buyer_recorded. */
|
|
47
|
+
signer: KeySignerSchema.optional(),
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* A payment rail's own record of a payment or refund (schema 1.5), for example an A2A-SE escrow attestation with its
|
|
51
|
+
* Merkle inclusion proof. `scheme` names the format; `record` is the rail's record exactly as received.
|
|
52
|
+
*/
|
|
53
|
+
export const RailAttestationSchema = z.strictObject({
|
|
54
|
+
scheme: z.string().min(1).max(200),
|
|
55
|
+
record: z.record(z.string(), z.unknown()),
|
|
56
|
+
});
|
|
57
|
+
export const RAIL_ATTESTED_EVENT_TYPES = ["payment_reported", "refund"];
|
|
11
58
|
export const PAYERS = ["buyer", "provider", "other"];
|
|
12
59
|
export const DELIVERY_EVENT_TYPES = ["acceptance", "completion", "partial_completion", "cancellation", "provider_failure", "terms_update", "correction"];
|
|
60
|
+
/** Outcome claims the provider may sign with its bound key (schema 1.5): how its work ended. */
|
|
61
|
+
export const SIGNED_CLAIM_TYPES = ["completion", "partial_completion", "cancellation", "provider_failure"];
|
|
13
62
|
/**
|
|
14
63
|
* Who made a delivery statement. Callers record "buyer" or "provider" (relayed by the buyer). The other values are
|
|
15
64
|
* recorded only from clearing-network events: "clearing_policy" for a decision computed by the agreed policy from
|
|
@@ -29,6 +78,28 @@ export const EXCEPTION_KINDS = [
|
|
|
29
78
|
"refund_after_close",
|
|
30
79
|
"allocation_changed_after_close",
|
|
31
80
|
"incomplete_lineage",
|
|
81
|
+
"usage_unpriced",
|
|
82
|
+
"usage_cost_mismatch",
|
|
83
|
+
/** A refund exceeds the agreed post-settlement cap or window, or a refund the terms require was not made. */
|
|
84
|
+
"refund_terms_breach",
|
|
85
|
+
/** A quote, invoice or charge names a different skill from the one delegated. */
|
|
86
|
+
"skill_price_mismatch",
|
|
87
|
+
/** Fewer independent witnesses attested to the run than the delegation requires. */
|
|
88
|
+
"witness_quorum_not_met",
|
|
89
|
+
/** Signed provider statements bound to the same receipt contradict each other. */
|
|
90
|
+
"conflicting_statements",
|
|
91
|
+
/** An estimate or hold matched no task or delegation, or several. */
|
|
92
|
+
"unmatched_estimate",
|
|
93
|
+
/** Actual cost on a node exceeds its estimate (beyond the task's tolerance). Recorded, not prevented. */
|
|
94
|
+
"actual_exceeds_estimate",
|
|
95
|
+
/** Actual cost on a node exceeds what is held for it. Recorded, not prevented. */
|
|
96
|
+
"actual_exceeds_hold",
|
|
97
|
+
/** A hold is still open past its expiry, or at close with nothing charged. */
|
|
98
|
+
"hold_not_released",
|
|
99
|
+
/** An estimate was issued after the first charge on its node; it is kept but not used as the estimate. */
|
|
100
|
+
"estimate_after_charge",
|
|
101
|
+
/** A cancelled or failed delegation still has billed cost that no refund, credit or reversal nets to zero. */
|
|
102
|
+
"charge_after_cancellation",
|
|
32
103
|
];
|
|
33
104
|
/** Trust labels kept separate on every screen and export (PRD §16 "Identity assurance"). Never collapsed into "verified". */
|
|
34
105
|
export const ASSURANCE_LABELS = [
|
|
@@ -46,11 +117,28 @@ export const ASSURANCE_LABELS = [
|
|
|
46
117
|
"expired",
|
|
47
118
|
/** A response statement revoked by a later statement from the same provider (schema 1.4). */
|
|
48
119
|
"revoked",
|
|
120
|
+
/** An estimate or hold signed by a budget gateway's bound key (schema 1.5). */
|
|
121
|
+
"gateway_signed",
|
|
122
|
+
/** A payment or refund backed by the rail's own attestation, verified offline (schema 1.5). Not operator-reported. */
|
|
123
|
+
"rail_attested",
|
|
49
124
|
];
|
|
50
125
|
export const RESPONSE_TYPES = ["acknowledge_view", "acknowledge_delivery", "submit_evidence", "propose_correction", "signed_attestation"];
|
|
51
|
-
|
|
126
|
+
/** "witness_attestation" (schema 1.5) lets an independent witness sign a statement that it observed the run. */
|
|
127
|
+
export const SHARE_ACTIONS = ["view", "acknowledge_delivery", "submit_evidence", "propose_correction", "signed_attestation", "witness_attestation"];
|
|
52
128
|
/** Receipt fields a provider may attest to or contest. Attesting one never implies the others (acceptance 16). */
|
|
53
|
-
export const ATTESTABLE_FIELDS = ["delivery.status", "delivery.evidence", "scope.terms_digest", "financial.amounts", "financial.status"];
|
|
129
|
+
export const ATTESTABLE_FIELDS = ["delivery.status", "delivery.evidence", "scope.terms_digest", "financial.amounts", "financial.status", "delivery.usage"];
|
|
130
|
+
/** Schema 1.5 added "delivery.usage". Documents of earlier versions disclose exactly these fields. */
|
|
131
|
+
const ATTESTABLE_FIELDS_BEFORE_1_5 = ["delivery.status", "delivery.evidence", "scope.terms_digest", "financial.amounts", "financial.status"];
|
|
132
|
+
export function attestableFieldsFor(schemaVersion) {
|
|
133
|
+
return ["1.2", "1.3", "1.4"].includes(schemaVersion) ? ATTESTABLE_FIELDS_BEFORE_1_5 : ATTESTABLE_FIELDS;
|
|
134
|
+
}
|
|
135
|
+
/** The usage of a run as recorded on a delivery claim: the trace's summary and the digest of the full trace, which stays with its holder. */
|
|
136
|
+
export const UsageRecordSchema = z.strictObject({
|
|
137
|
+
trace_digest: Digest,
|
|
138
|
+
summary: UsageSummarySchema,
|
|
139
|
+
});
|
|
140
|
+
/** Delivery claims that may carry usage. */
|
|
141
|
+
export const USAGE_CLAIM_TYPES = ["completion", "partial_completion"];
|
|
54
142
|
/** Evidence stays in its source system; only references and digests are stored. Script-capable schemes are refused. */
|
|
55
143
|
export const EvidenceRefSchema = z.object({
|
|
56
144
|
uri: z
|
|
@@ -61,6 +149,13 @@ export const EvidenceRefSchema = z.object({
|
|
|
61
149
|
digest: Digest.nullable(),
|
|
62
150
|
evidence_type: z.string().min(1).max(100),
|
|
63
151
|
});
|
|
152
|
+
/** Why part of a delegation chain was not captured. broken_edge: a reported sub-task arrived with no outcome. */
|
|
153
|
+
export const CAPTURE_GAP_KINDS = ["capture_failed", "queue_overflow", "provider_undisclosed", "manual_gap", "broken_edge"];
|
|
154
|
+
export const CaptureGapInputSchema = z.object({
|
|
155
|
+
delegation_id: z.string().nullable().default(null),
|
|
156
|
+
kind: z.enum(CAPTURE_GAP_KINDS),
|
|
157
|
+
detail: z.string().min(1).max(2000),
|
|
158
|
+
});
|
|
64
159
|
export const TaskInputSchema = z.object({
|
|
65
160
|
external_ref: z.string().min(1).max(200),
|
|
66
161
|
currency: Currency,
|
|
@@ -72,12 +167,25 @@ export const TaskInputSchema = z.object({
|
|
|
72
167
|
shared_description: z.string().max(1000).nullable().default(null),
|
|
73
168
|
retrospective: z.boolean().default(false),
|
|
74
169
|
occurred_at: Iso.nullable().default(null),
|
|
170
|
+
/** Schema 1.5: how far actual cost may exceed an estimate, in basis points, before actual_exceeds_estimate opens. */
|
|
171
|
+
estimate_tolerance_bps: z.number().int().min(0).max(100_000).optional(),
|
|
75
172
|
});
|
|
76
173
|
export const ProviderInputSchema = z.object({
|
|
77
174
|
name: z.string().min(1).max(200),
|
|
78
175
|
provider_own_id: z.string().max(200).nullable().default(null),
|
|
79
176
|
domain: z.string().max(253).nullable().default(null),
|
|
80
177
|
});
|
|
178
|
+
/**
|
|
179
|
+
* Independent witnesses a delegation requires (schema 1.5). A witness is a registered provider other than the
|
|
180
|
+
* delegation's own, signing with a domain-challenged key; it counts only if that registrable domain differs from the
|
|
181
|
+
* provider's, the buyer operator's and every other counted witness's. Too few raise witness_quorum_not_met.
|
|
182
|
+
*/
|
|
183
|
+
export const SubledgerWitnessPolicySchema = z.strictObject({
|
|
184
|
+
min_independent_witnesses: z.number().int().min(1).max(10),
|
|
185
|
+
/** If set, only these providers may witness. */
|
|
186
|
+
witness_provider_ids: z.array(z.string().min(1)).min(1).max(20).optional(),
|
|
187
|
+
independence: z.literal("distinct_verified_domain"),
|
|
188
|
+
});
|
|
81
189
|
export const DelegationInputSchema = z.object({
|
|
82
190
|
parent_delegation_id: z.string().nullable().default(null),
|
|
83
191
|
provider_id: z.string().nullable().default(null),
|
|
@@ -100,6 +208,12 @@ export const DelegationInputSchema = z.object({
|
|
|
100
208
|
retrospective: z.boolean().default(false),
|
|
101
209
|
/** The provider's run as the buyer recorded it (for A2A, from the task and agent card). Provider statements can cite it. */
|
|
102
210
|
execution: ExecutionDescriptorSchema.optional(),
|
|
211
|
+
/** Agreed usage prices (schema 1.5). Billed cost is compared with usage priced at these rates. */
|
|
212
|
+
pricing: PricingSchema.optional(),
|
|
213
|
+
/** Agreed refund and failure terms (schema 1.5). Refunds that break them raise refund_terms_breach. */
|
|
214
|
+
refund_terms: RefundTermsSchema.optional(),
|
|
215
|
+
/** Independent witnesses required (schema 1.5). */
|
|
216
|
+
witness_policy: SubledgerWitnessPolicySchema.optional(),
|
|
103
217
|
});
|
|
104
218
|
export const DelegationEventInputSchema = z.object({
|
|
105
219
|
type: z.enum(DELIVERY_EVENT_TYPES),
|
|
@@ -117,6 +231,12 @@ export const DelegationEventInputSchema = z.object({
|
|
|
117
231
|
terms_digest: Digest.nullable().optional(),
|
|
118
232
|
expected_delivery: Iso.nullable().optional(),
|
|
119
233
|
downstream_visibility: z.enum(["unknown", "disclosed", "none"]).optional(),
|
|
234
|
+
/** Schema 1.5: replaces the delegation's pricing; null removes it. */
|
|
235
|
+
pricing: PricingSchema.nullable().optional(),
|
|
236
|
+
/** Schema 1.5: replaces the delegation's refund terms; null removes them. */
|
|
237
|
+
refund_terms: RefundTermsSchema.nullable().optional(),
|
|
238
|
+
/** Schema 1.5: replaces the delegation's witness policy; null removes it. */
|
|
239
|
+
witness_policy: SubledgerWitnessPolicySchema.nullable().optional(),
|
|
120
240
|
})
|
|
121
241
|
.nullable()
|
|
122
242
|
.default(null),
|
|
@@ -125,7 +245,29 @@ export const DelegationEventInputSchema = z.object({
|
|
|
125
245
|
reason: z.string().max(2000).nullable().default(null),
|
|
126
246
|
retrospective: z.boolean().default(false),
|
|
127
247
|
occurred_at: Iso.nullable().default(null),
|
|
248
|
+
/** completion and partial_completion only (schema 1.5): the run's usage, from its trace. */
|
|
249
|
+
usage: UsageRecordSchema.optional(),
|
|
250
|
+
/**
|
|
251
|
+
* Outcome claims asserted by the provider only (schema 1.5): the provider's signature over the outcome statement,
|
|
252
|
+
* which names the delegation's provider_job_ref (for A2A, the task id) and the claim's evidence.
|
|
253
|
+
*/
|
|
254
|
+
signer: KeySignerSchema.optional(),
|
|
128
255
|
});
|
|
256
|
+
/** Rules across fields of a delivery event that the schema cannot express. */
|
|
257
|
+
export function delegationEventProblems(input) {
|
|
258
|
+
const problems = [];
|
|
259
|
+
if (input.usage !== undefined && !USAGE_CLAIM_TYPES.includes(input.type))
|
|
260
|
+
problems.push(`usage is allowed only on ${USAGE_CLAIM_TYPES.join(" and ")} events`);
|
|
261
|
+
if (input.signer !== undefined) {
|
|
262
|
+
if (!SIGNED_CLAIM_TYPES.includes(input.type))
|
|
263
|
+
problems.push(`signer is allowed only on ${SIGNED_CLAIM_TYPES.join(", ")} events`);
|
|
264
|
+
if (input.asserted_by !== "provider")
|
|
265
|
+
problems.push("a signed claim must be asserted_by provider");
|
|
266
|
+
if (input.usage !== undefined)
|
|
267
|
+
problems.push("a signed claim cannot carry usage, which the outcome statement does not cover");
|
|
268
|
+
}
|
|
269
|
+
return problems;
|
|
270
|
+
}
|
|
129
271
|
export const FxSchema = z.object({
|
|
130
272
|
base_currency: Currency,
|
|
131
273
|
quote_currency: Currency,
|
|
@@ -155,6 +297,12 @@ export const FinancialEventInputSchema = z
|
|
|
155
297
|
settles_event_id: z.string().nullable().default(null),
|
|
156
298
|
fx: FxSchema.nullable().default(null),
|
|
157
299
|
reason: z.string().max(2000).nullable().default(null),
|
|
300
|
+
/** Quotes, invoices and charges only (schema 1.5): the skill billed, from the provider's billing line. */
|
|
301
|
+
skill: SkillRefSchema.optional(),
|
|
302
|
+
/** Estimates and holds only (schema 1.5): who issued it, why, and how it relates to earlier ones. */
|
|
303
|
+
expectation: ExpectationSchema.optional(),
|
|
304
|
+
/** Payments and refunds only (schema 1.5): the rail's own attestation of the payment, embedded so it verifies offline. */
|
|
305
|
+
rail_attestation: RailAttestationSchema.optional(),
|
|
158
306
|
/** Stable references used for matching. Only a unique match is applied automatically. */
|
|
159
307
|
match: z
|
|
160
308
|
.object({
|
|
@@ -175,7 +323,32 @@ export const FinancialEventInputSchema = z
|
|
|
175
323
|
issue.addIssue({ code: "custom", path: ["reverses_event_id"], message: "reversal needs reverses_event_id" });
|
|
176
324
|
if (e.type === "reversal" && !e.reason)
|
|
177
325
|
issue.addIssue({ code: "custom", path: ["reason"], message: "reversal needs a reason" });
|
|
326
|
+
if (e.skill && !SKILL_BILLED_EVENT_TYPES.includes(e.type))
|
|
327
|
+
issue.addIssue({ code: "custom", path: ["skill"], message: `skill is allowed only on ${SKILL_BILLED_EVENT_TYPES.join(", ")} events` });
|
|
328
|
+
for (const problem of expectationProblems(e))
|
|
329
|
+
issue.addIssue({ code: "custom", path: ["expectation"], message: problem });
|
|
330
|
+
if (e.rail_attestation && !RAIL_ATTESTED_EVENT_TYPES.includes(e.type)) {
|
|
331
|
+
issue.addIssue({ code: "custom", path: ["rail_attestation"], message: `rail_attestation is allowed only on ${RAIL_ATTESTED_EVENT_TYPES.join(" and ")} events` });
|
|
332
|
+
}
|
|
178
333
|
});
|
|
334
|
+
/** Rules for estimates and holds that the schema cannot express. */
|
|
335
|
+
export function expectationProblems(e) {
|
|
336
|
+
const isExpectation = EXPECTATION_EVENT_TYPES.includes(e.type);
|
|
337
|
+
if (!isExpectation)
|
|
338
|
+
return e.expectation ? ["expectation is allowed only on estimate and hold events"] : [];
|
|
339
|
+
if (!e.expectation)
|
|
340
|
+
return [`${e.type} events need expectation`];
|
|
341
|
+
const problems = [];
|
|
342
|
+
if (e.amount_minor < 0)
|
|
343
|
+
problems.push(`${e.type} amount must not be negative`);
|
|
344
|
+
if (e.type === "hold" && !e.expectation.hold_status)
|
|
345
|
+
problems.push("hold events need expectation.hold_status");
|
|
346
|
+
if (e.type === "estimate" && e.expectation.hold_status)
|
|
347
|
+
problems.push("only hold events carry hold_status");
|
|
348
|
+
if (e.expectation.issued_by === "operator" && e.expectation.signer)
|
|
349
|
+
problems.push("operator estimates and holds are recorded by the operator, not signed by a provider key");
|
|
350
|
+
return problems;
|
|
351
|
+
}
|
|
179
352
|
export const AllocationTargetSchema = z.object({
|
|
180
353
|
type: z.enum(["task", "delegation", "cost_center", "unallocated"]),
|
|
181
354
|
id: z.string().max(200).nullable(),
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Pricing } from "@atcn/schema";
|
|
2
|
+
import type { DeliveryClaim, ResponseRecord, UsageCheck } from "./documents.js";
|
|
3
|
+
import type { Rollup, Totals } from "./rollup.js";
|
|
4
|
+
/** What a delegation was billed: invoices, charges, fees and adjustments, less credits and refunds. */
|
|
5
|
+
export declare function billedMinor(totals: Totals | undefined): number;
|
|
6
|
+
/**
|
|
7
|
+
* The usage claims that count for one delegation: completion and partial_completion claims with usage that no later
|
|
8
|
+
* correction superseded. Each claim reports the usage of the work it covers, so claims are summed, but a trace
|
|
9
|
+
* recorded more than once (for example by both buyer and provider) counts once, from its latest claim.
|
|
10
|
+
*/
|
|
11
|
+
export declare function countedUsageClaims(claims: DeliveryClaim[]): DeliveryClaim[];
|
|
12
|
+
export interface UsageCheckInput {
|
|
13
|
+
delegation_id: string;
|
|
14
|
+
currency: string;
|
|
15
|
+
pricing: Pricing | null | undefined;
|
|
16
|
+
/** This delegation's claims, in recorded order. */
|
|
17
|
+
claims: DeliveryClaim[];
|
|
18
|
+
billed_minor: number;
|
|
19
|
+
/** Whether a provider key-signed, unexpired, unrevoked statement attests delivery.usage on this delegation's receipt. */
|
|
20
|
+
provider_attested: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Prices a delegation's usage at its agreed rates and compares it with what was billed, in both directions:
|
|
24
|
+
* overbilling is the obvious risk, and underbilling often means a charge is missing or attributed elsewhere.
|
|
25
|
+
* Null when the delegation has no pricing or no counted usage.
|
|
26
|
+
*/
|
|
27
|
+
export declare function usageCheckFor(input: UsageCheckInput): UsageCheck | null;
|
|
28
|
+
/** Delegations whose receipts carry an in-force provider key-signed attestation of delivery.usage. */
|
|
29
|
+
export declare function usageAttestedDelegations(responses: ResponseRecord[], receipts: {
|
|
30
|
+
receipt_id: string;
|
|
31
|
+
delegation_id: string;
|
|
32
|
+
}[]): Set<string>;
|
|
33
|
+
/** Usage checks for every delegation of a closure with pricing and counted usage, in delegation order. */
|
|
34
|
+
export declare function usageChecksFor(delegations: {
|
|
35
|
+
delegation_id: string;
|
|
36
|
+
currency: string;
|
|
37
|
+
pricing?: Pricing;
|
|
38
|
+
}[], claims: DeliveryClaim[], rollup: Rollup, responses: ResponseRecord[], receipts: {
|
|
39
|
+
receipt_id: string;
|
|
40
|
+
delegation_id: string;
|
|
41
|
+
}[]): UsageCheck[];
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { allowedDifference, expectedCostFromUsage } from "@atcn/schema";
|
|
2
|
+
import { ASSURANCE_LABELS, USAGE_CLAIM_TYPES } from "./types.js";
|
|
3
|
+
/** What a delegation was billed: invoices, charges, fees and adjustments, less credits and refunds. */
|
|
4
|
+
export function billedMinor(totals) {
|
|
5
|
+
return totals ? totals.invoiced + totals.charged + totals.fees + totals.adjustments - totals.credits - totals.refunded : 0;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The usage claims that count for one delegation: completion and partial_completion claims with usage that no later
|
|
9
|
+
* correction superseded. Each claim reports the usage of the work it covers, so claims are summed, but a trace
|
|
10
|
+
* recorded more than once (for example by both buyer and provider) counts once, from its latest claim.
|
|
11
|
+
*/
|
|
12
|
+
export function countedUsageClaims(claims) {
|
|
13
|
+
const superseded = new Set(claims.map((c) => c.supersedes_event_id).filter((id) => id !== null));
|
|
14
|
+
const byTrace = new Map();
|
|
15
|
+
for (const c of claims) {
|
|
16
|
+
if (!c.usage || superseded.has(c.event_id) || !USAGE_CLAIM_TYPES.includes(c.type))
|
|
17
|
+
continue;
|
|
18
|
+
byTrace.set(c.usage.trace_digest, c);
|
|
19
|
+
}
|
|
20
|
+
return [...byTrace.values()];
|
|
21
|
+
}
|
|
22
|
+
function sortedLabels(labels) {
|
|
23
|
+
return ASSURANCE_LABELS.filter((l) => labels.has(l));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Prices a delegation's usage at its agreed rates and compares it with what was billed, in both directions:
|
|
27
|
+
* overbilling is the obvious risk, and underbilling often means a charge is missing or attributed elsewhere.
|
|
28
|
+
* Null when the delegation has no pricing or no counted usage.
|
|
29
|
+
*/
|
|
30
|
+
export function usageCheckFor(input) {
|
|
31
|
+
if (!input.pricing)
|
|
32
|
+
return null;
|
|
33
|
+
const counted = countedUsageClaims(input.claims);
|
|
34
|
+
if (counted.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
const cost = expectedCostFromUsage(input.pricing, counted.map((c) => c.usage.summary));
|
|
37
|
+
const expected = cost.expected_minor;
|
|
38
|
+
const allowed = expected === null ? null : allowedDifference(expected, input.pricing.tolerance_bps);
|
|
39
|
+
const difference = expected === null ? null : input.billed_minor - expected;
|
|
40
|
+
const labels = new Set(counted.flatMap((c) => c.assurance).filter((l) => l !== "superseded"));
|
|
41
|
+
if (input.provider_attested)
|
|
42
|
+
labels.add("provider_key_signed");
|
|
43
|
+
return {
|
|
44
|
+
delegation_id: input.delegation_id,
|
|
45
|
+
currency: input.currency,
|
|
46
|
+
expected_minor: expected,
|
|
47
|
+
lines: cost.lines,
|
|
48
|
+
billed_minor: input.billed_minor,
|
|
49
|
+
difference_minor: difference,
|
|
50
|
+
allowed_difference_minor: allowed,
|
|
51
|
+
within_tolerance: difference === null || allowed === null ? null : Math.abs(difference) <= allowed,
|
|
52
|
+
unpriced: cost.unpriced,
|
|
53
|
+
trace_digests: counted.map((c) => c.usage.trace_digest).sort(),
|
|
54
|
+
assurance: sortedLabels(labels),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Delegations whose receipts carry an in-force provider key-signed attestation of delivery.usage. */
|
|
58
|
+
export function usageAttestedDelegations(responses, receipts) {
|
|
59
|
+
const delegationOf = new Map(receipts.map((r) => [r.receipt_id, r.delegation_id]));
|
|
60
|
+
const attested = new Set();
|
|
61
|
+
for (const r of responses) {
|
|
62
|
+
const inForce = r.assurance.includes("provider_key_signed") && !r.assurance.includes("expired") && !r.assurance.includes("revoked");
|
|
63
|
+
if (inForce && r.statement.response_type === "signed_attestation" && r.statement.fields.includes("delivery.usage")) {
|
|
64
|
+
const delegationId = delegationOf.get(r.receipt_id);
|
|
65
|
+
if (delegationId)
|
|
66
|
+
attested.add(delegationId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return attested;
|
|
70
|
+
}
|
|
71
|
+
/** Usage checks for every delegation of a closure with pricing and counted usage, in delegation order. */
|
|
72
|
+
export function usageChecksFor(delegations, claims, rollup, responses, receipts) {
|
|
73
|
+
const attested = usageAttestedDelegations(responses, receipts);
|
|
74
|
+
return delegations.flatMap((d) => {
|
|
75
|
+
const node = rollup.nodes.find((n) => n.node_id === d.delegation_id);
|
|
76
|
+
const result = usageCheckFor({
|
|
77
|
+
delegation_id: d.delegation_id,
|
|
78
|
+
currency: d.currency,
|
|
79
|
+
pricing: d.pricing,
|
|
80
|
+
claims: claims.filter((c) => c.delegation_id === d.delegation_id),
|
|
81
|
+
billed_minor: billedMinor(node?.direct[d.currency]),
|
|
82
|
+
provider_attested: attested.has(d.delegation_id),
|
|
83
|
+
});
|
|
84
|
+
return result ? [result] : [];
|
|
85
|
+
});
|
|
86
|
+
}
|
package/dist/verify.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { type PublicKeyRecord } from "@atcn/schema";
|
|
2
|
-
import { type OperatorKeyRecord } from "./documents.js";
|
|
2
|
+
import { type OperatorKeyRecord, type SignedClosure } from "./documents.js";
|
|
3
|
+
import { type DerivedException } from "./exceptions.js";
|
|
3
4
|
export interface CheckResult {
|
|
4
5
|
name: string;
|
|
5
6
|
ok: boolean;
|
|
6
7
|
details: string[];
|
|
8
|
+
/** Set when the check had nothing it could inspect (for example, traces committed but not supplied). Not a pass. */
|
|
9
|
+
state?: "not_inspected";
|
|
7
10
|
}
|
|
8
11
|
export interface SubledgerVerificationReport {
|
|
9
12
|
valid: boolean;
|
|
@@ -25,8 +28,12 @@ export interface SubledgerVerifyOptions {
|
|
|
25
28
|
obligationPackages?: unknown[];
|
|
26
29
|
/** Time to check a receipt's expires_at against (ISO 8601). Defaults to now. */
|
|
27
30
|
at?: string;
|
|
31
|
+
/** Trace files (raw bytes) behind recorded usage, to recompute each usage summary from its trace. */
|
|
32
|
+
traces?: Uint8Array[];
|
|
28
33
|
}
|
|
29
34
|
/** Detects the document type and verifies it offline: no network calls, no service access (acceptance 9). */
|
|
30
35
|
export declare function verifySubledgerDocument(input: unknown, options: SubledgerVerifyOptions): SubledgerVerificationReport;
|
|
31
36
|
export declare function verifyReceipt(input: unknown, options: SubledgerVerifyOptions): SubledgerVerificationReport;
|
|
32
37
|
export declare function verifyClosure(input: unknown, options: SubledgerVerifyOptions): SubledgerVerificationReport;
|
|
38
|
+
/** The derived exceptions a closure's own records imply at close, as of generated_at; the service must list each as open or resolved. */
|
|
39
|
+
export declare function closureDerivedExceptions(c: SignedClosure["payload"]): DerivedException[];
|