@atrib/verify
v0.2.6
Published
Verifier library for atrib. Independent signature, graph-derivation, and settlement-recommendation checks per atrib spec §4.6.
Maintainers
Readme
@atrib/verify
Independent verification of atrib records and settlement documents. Re-runs the spec §4.6 calculation algorithm locally and checks the result against what a recommendation document claims. Verifies any signed record against its creator key. No trust in any intermediary required.
This is the verifier half of the atrib protocol, used by merchants closing transactions, auditors checking agent activity, regulators querying historical state, and any party that needs to validate atrib data independently. The agent and tool servers produce signed attribution records. The Merkle log stores them. This package answers the questions any verifier has to answer: given the graph and the policy, is this distribution actually correct? Was this record actually signed by the key it claims? Did this action actually happen at the time it claims?
Quick start
import { AtribVerifier } from '@atrib/verify'
const verifier = new AtribVerifier({
merchantKey: process.env.ATRIB_MERCHANT_KEY, // optional, base64url Ed25519 seed
graphEndpoint: 'https://graph.atrib.dev/v1', // defaults to atrib.dev endpoints
logEndpoint: 'https://log.atrib.dev/v1',
})
const result = await verifier.verify(recommendationDoc)
// {
// valid: true,
// signatureOk: true,
// calcMatch: true,
// distribution: { 'sha256:...': 0.4, 'sha256:...': 0.6 },
// warnings: [],
// graph_node_count: 7,
// }valid === true means both the document's Ed25519 signature verified against the calculator's published key and the local recalculation produced the same distribution within 1e-9. Either failing flips valid to false and the specific failure is reported in signatureOk / calcMatch / warnings so you know exactly what went wrong.
What verify() actually does (per spec §5.5.2)
- Resolves the calculator's public key from
recommendationDoc.calculated_by. For the well-knownresolve.atrib.devservice, the key is fetched from the/pubkeyendpoint. For other calculators, the merchant supplies the key out-of-band. - Verifies the Ed25519 signature over the JCS-canonicalized recommendation document (excluding the
signaturefield). - Fetches the attribution graph at
recommendationDoc.graph_tree_sizefrom the configured graph endpoint. Pinning to a specific tree size makes the verification reproducible; the graph is fixed, not "live." - Fetches the session policy record referenced by
policy_record_id, or uses the spec §4.3 default policy ifpolicy_record_id === 'default'. - Re-runs
calculate(graph, policy, sessionPolicyRecord)locally; a pure function with no network calls and no randomness. - Compares distributions using
distributionsMatch()(within1e-9per recipient, accounting for floating-point drift across implementations).
The key invariant per spec §4.6: any party with the same graph and the same policy MUST get the same distribution. If they don't, either the calculator cheated, the document was tampered with, or one party has a buggy implementation. Either way the merchant should not pay against this document.
Post-hoc calculation (§5.5.3)
If the agent that drove the session was not atrib-aware (no @atrib/agent middleware), the merchant can still produce a signed recommendation after the fact, as long as the tools were attributed:
const recommendation = await verifier.calculate({
context_id: 'sess_abc123...',
policy: 'default', // or a full PolicyDocument
signWith: 'merchant', // signs with merchantKey if present
})
// → fully-shaped RecommendationDocument, ready to settle againstPer the §5.8 degradation contract, this never throws on a missing key; if signWith === 'merchant' but merchantKey is unset, the document is returned unsigned with a warning rather than crashing the merchant pipeline.
API reference
new AtribVerifier(options)
| Field | Type | Default | Description |
| ----------------- | -------- | ----------------------------- | ----------------------------------------------------------------------- |
| logEndpoint | string | https://log.atrib.dev/v1 | The Merkle log to fetch checkpoints and proofs from. |
| graphEndpoint | string | https://graph.atrib.dev/v1 | The graph query endpoint (spec §3). |
| resolveEndpoint | string | https://resolve.atrib.dev/v1 | Reserved for v2 remote calculation. |
| merchantKey | string | unset | Base64url Ed25519 32-byte seed. Optional. verify() works without it. |
verify(doc): Promise<VerificationResult>
Independently re-runs the §4.6 calculation and verifies the document signature. Always returns a result object; never throws. Inspect valid, signatureOk, calcMatch, and warnings to understand the outcome.
This method operates on RecommendationDocument shapes (settlement-recommendation flow per spec §5.5.2). For verifying individual AtribRecords, see verifyRecord below.
verifyRecord(record, options): Promise<RecordVerificationResult>
Per-record verification. Verifies a single signed record's Ed25519 signature and surfaces per-record annotations defined by spec sections §1.2.5 (D041), §1.2.6 (D044), §6.7 (D051), and §8.4 (D045).
import { verifyRecord } from '@atrib/verify'
const result = await verifyRecord(record, {
upstreamCandidate, // optional, for provenance_token resolution
informedByCandidates: [], // optional, for informed_by[] resolution
identityClaim, // optional, for capability_check (caller does directory lookup)
})
// result: {
// valid: boolean
// signatureOk: boolean
// posture: { timestamp_granularity, timestamp_consistent, timestamp_granularity_explicit }
// provenance?: { token, upstream_record_hash, upstream_resolved }
// informed_by_resolution?: { resolved: string[], dangling: string[] }
// capability_check?: { envelope, in_envelope, mismatches, unresolvable }
// warnings: string[]
// }Implemented per-record annotations:
provenance:{ token, upstream_record_hash, upstream_resolved }per session-genesis record carryingprovenance_token(D044 / §1.2.6). The 16-byte token truncation is irreversible:upstream_record_hashpopulates only when the caller supplies a candidate whose canonical-form SHA-256[:16] matches the token.informed_by_resolution:{ resolved: string[], dangling: string[] }per record carryinginformed_by(D041 / §1.2.5). Dangling references are flagged but do not fail verification: they signal "the verifier has not seen upstream context," not "the record is invalid."posture:{ timestamp_granularity, timestamp_consistent, timestamp_granularity_explicit, args_commitment_form, result_commitment_form, tool_name_form }(D045 / D061 / §8.2 / §8.3 / §8.4). Always populated. Surfaces (a) the declared timing granularity, whether the timestamp value structurally matches the spec's trailing-zero invariant, and whether the field was explicitly set vs defaulted; (b) the structurally-detectedargs_hash/result_hashcommitment scheme:'salted-sha256'whenargs_salt/result_saltis present,'plain-sha256'otherwise (the'hmac-sha256'variant from §8.3 is signaled out-of-band and is not structurally detectable); and (c) the §8.2tool_name_form:'hashed'whentool_namematches^sha256:[0-9a-f]{64}$,'plain'for any other present value,nullwhen the field is absent. Per D061 the §8.2 verbatim-vs-opaque distinction is NOT structurally detectable — both surface as'plain'.capability_check:{ envelope, in_envelope, mismatches, unresolvable }(D051 / §6.7). Populated only when the caller passes a resolvedidentityClaimin options. Checks the record'sevent_typeagainst the envelope'sevent_typesallowlist and the record'stimestampagainstexpires_at.tool_names(against tool_call records),max_amount, andcounterparties(against transaction records) flagunresolvable: truebecause the constraints depend on data not yet on the standard record shape (tool_name) or out-of-band protocol events (payment amount + counterparty). Per §6.7.3 out-of-envelope is a signal, not invalidation: mismatches do not flipvalidto false. The caller is responsible for fetching the active envelope at the record's timestamp via@atrib/directory'slookup()(or a cached equivalent);@atrib/verifyintentionally has no@atrib/directorydependency.cross_attestation:{ signers_count, signers_valid, missing }(D052 / §1.7.6). Populated only on transaction records (event_type = transaction). Each entry insigners[]is verified against the cross-attestation canonical bytes (JCS form withsigners: []and the top-levelsignaturefield omitted, per §1.7.6).missing: truewhen fewer than 2 signers verify — atrib's normative minimum. Per §1.7.6 missing is a SIGNAL not invalidation:validstays true if the underlying signature path holds. Legacy single-signer transaction records (nosigners[]array, only top-levelsignature) surface assigners_count: 0, missing: trueso consumers can flag them while accepting the cryptographic validity.
Pending per-record annotations (tracked as a Pending decision in DECISIONS.md P005):
cross_log_proof_count/cross_log_threshold_met/cross_log_equivocation_detected(D050 / §2.11): requires multi-log proof-bundle parsing and trusted-log-set config. (Note:tool_name_form,args_commitment_form, andresult_commitment_formper §8.2/§8.3 are all now implemented underpostureabove. D061 addedtool_name,args_hash, andresult_hashto the §1.2.1 canonical record schema, completing the structural inputs.)
Each pending annotation is its own ADR scope when external consumers need it.
calculate(options): Promise<RecommendationDocument>
Post-hoc calculation when no agent SDK was present. Always returns a fully-shaped document, unsigned with a warning if the merchant key is missing.
Lower-level primitives
For advanced use (custom calculators, alternative signing flows), the package also exports:
calculate(graph, policy, sessionPolicyRecord): the pure §4.6 calculation functionDEFAULT_POLICY: the spec §4.3 default policy documentisValidPolicy(doc): schema check forPolicyDocumentsignRecommendation(unsigned, privateKey): JCS + Ed25519 signingverifyRecommendationSignature(doc, publicKey): signature verificationrecommendationSigningInput(doc): the canonical bytes that get signeddistributionsMatch(a, b): float-tolerant equality (within1e-9per recipient)fetchGraph(endpoint, contextId, treeSize?),fetchSessionPolicyRecord,fetchPolicyDocument
Why pure functions matter
The §4.6 calculation algorithm is intentionally a pure function of (graph, policy):
- No network calls during calculation. The graph and policy are fetched up front and then
calculate()runs in-memory. - No timestamps beyond those already embedded in the records. Two runs an hour apart on the same inputs produce the same output.
- No randomness. No "tie-breaker by hash of current time" or anything like that. Ties are broken deterministically per the spec.
- No floating-point ordering surprises. The algorithm walks the graph in a deterministic order so two implementations on identical input produce identical output (within
1e-9for the final distribution shares).
This is what makes verification possible: the merchant's local recalculation is the same code the calculator ran, producing the same output, so any disagreement is a real signal; not implementation drift.
§5.8 degradation contract
Per the absolute invariant (also enforced in @atrib/mcp and @atrib/agent), atrib failures never break the host:
- Missing or invalid
merchantKey→ constructor logsatrib: ...warning,merchantPrivateKey = null, no throw. verify()errors during signature resolution, graph fetch, or calculation are caught and surfaced aswarnings: string[]withvalid = false.calculate({ signWith: 'merchant' })with a missing key returns an unsigned document plus a warning, rather than throwing.
The merchant's payment pipeline never crashes because of an atrib problem. It just gets valid: false and decides what to do with that.
Test coverage
184 tests across 10 test files covering the §4.6 calculation algorithm, graph endpoint client, JCS canonicalization, Ed25519 signing, settlement recommendations, policy templates, policy builder, calculation edge cases, property-based testing with fast-check, and full verify() / calculate() paths including §5.8 degradation.
Run them with pnpm --filter @atrib/verify test.
Spec references
| Spec section | What this package implements |
| ------------ | ---------------------------------------------------- |
| §3 | Graph query interface (client side) |
| §4.3 | Default policy document |
| §4.6 | Pure calculation algorithm |
| §4.7 | Recommendation document signing/verification |
| §5.5 | AtribVerifier class. verify() and calculate() |
| §5.8 | Degradation contract; failures never break the host |
The full protocol spec is at atrib-spec.md.
See also
@atrib/mcp, server-side middleware that produces the signed recordsverify()ultimately validates@atrib/agent, agent-side interceptor + framework adapters@atrib/log-dev, development-mode Merkle log stub. Returns placeholder Merkle hashes that will not pass strict cryptographic verification, fine for end-to-end shape testing, not for production verification.packages/integration/examples/end-to-end/, runnable demo wiring everything togetherDECISIONS.md, architectural decision log
A note on documentation links. The atrib protocol repository is currently private (in-progress public preparation). Links in this README to the spec and sister packages (
atrib-spec.md,packages/agent/README.md, etc.) point atgithub.com/creatornader/atrib/blob/main/...URLs that will resolve once the repository goes public. Until then, seeatrib.devfor the protocol overview.
