@atrib/verify
v0.13.2
Published
Verifier library for atrib's verifiable action layer. Checks records, evidence, handoffs, graph derivation, and settlement documents.
Downloads
3,642
Maintainers
Readme
@atrib/verify
Independent verification for atrib's verifiable action layer. Checks signed records, evidence blocks, handoff packets, and settlement documents. Re-runs the payments profile §8 calculation algorithm (relocated from spec §4.6 per D147) locally and checks the result against what a recommendation document claims. Verification uses caller-pinned keys and policy rather than an intermediary's verdict.
This is the verifier half of the atrib protocol, used by merchants closing transactions, auditors checking agent activity, teams accepting handoffs, policy systems reviewing high-impact actions, 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 commitments to them. This package answers the questions a verifier can decide from supplied evidence: does the distribution recompute, did the claimed key sign these bytes, is the body consistent with its commitment, and which independent evidence corroborates the claim? It does not infer that a signed claim is true merely because its signature and hash verify.
Install
pnpm add @atrib/verifyVerify a local build with pnpm --filter @atrib/verify test.
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',
})
// recommendationDoc is a §4.7 settlement RecommendationDocument produced by a
// calculator or merchant (signed distribution over record hashes).
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
Payment-specific APIs now also ship at @atrib/verify/payments:
verifySettlementRecommendation, calculate, verifyAp2ViEvidence, and
verifyAp2ViEvidenceAsync. Root exports remain during the deprecation cycle.
Nostr and Buzz event evidence APIs ship at the package root:
deriveNostrEventId, verifyNostrEvent, verifyBuzzOwnerAttestation, and
verifyBuzzEvent. The Buzz verifier checks the event and optional NIP-OA
owner authorization. It does not infer community admission, relay retention,
audit inclusion, or runtime execution from a valid event.
Checkpoint witness acquisition also ships at the package root:
fetchCheckpointWitnessThreshold(). The caller pins the log name, log key,
checkpoint URL, and each witness name, key, and endpoint:
import { fetchCheckpointWitnessThreshold } from '@atrib/verify'
const result = await fetchCheckpointWitnessThreshold({
log: {
name: 'log.atrib.dev/v1',
publicKey: pinnedLogKey,
checkpointUrl: 'https://log.atrib.dev/v1/checkpoint',
},
witnesses: [
{
name: 'witness.example.org',
publicKey: pinnedWitnessKey,
baseUrl: 'https://witness.example.org',
},
],
requiredWitnesses: 1,
})
if (!result.threshold.thresholdMet) {
throw new Error(JSON.stringify(result.witnesses))
}The helper fetches cosignatures from the pinned witness endpoints, verifies them locally, and reports transport and verification results separately for each endpoint. It constructs a fresh note from the verified operator signature and endpoint-returned cosignatures. A witness-looking line delivered only by the log does not count. Responses are bounded to 16 KiB and requests time out after 10 seconds by default. Callers can override both limits.
Use fetchWitnessCosignaturesForCheckpoint() when a proof bundle already
contains the exact checkpoint that must be witnessed. resolveIdentity() uses
that path when trustedWitnessEndpoints is supplied, so a newer log
checkpoint cannot replace the checkpoint that covers the selected directory
anchor.
verifyWitnessRetirement() verifies an
atrib.witness-retirement.v1 artifact against the public key named inside it.
A valid artifact is revocation input for caller-owned trust policy. The caller
must remove that key and epoch from its accepted witness set. Verification of
the artifact alone does not mutate policy.
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, options): 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). When the caller supplies ap2ViEvidence, the verifier attaches the async AP2 / VI result as ap2_vi_evidence. That block is tiered and does not change the base recommendation signature or calculation checks. 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)
resolvedFacts, // optional, caller-resolved facts for capability_check
authorizationEvidence, // optional, OAuth/MCP, AAuth, or x401 evidence blocks
ap2ViEvidence, // optional, transaction-only AP2 / VI evidence bundle
ap2ViEvidenceOptions, // optional, passed to verifyAp2ViEvidenceAsync()
trustedCreatorKeys, // optional, trusted transaction signer keys
delegationCertificates, // optional, certificates for the §1.11 walk
delegationTrustedTimeMs: Date.now(), // host/verifier clock for expiry
delegationScopeFacts, // optional amount, counterparty, and usage facts
proofBundle, // optional, post-signing anchor proofs for this record
anchorTrust, // required with proofBundle; caller-owned anchor trust policy
})
// 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 }
// evidence?: EvidenceVerificationBlock[]
// ap2_vi_evidence?: Ap2ViEvidenceVerification
// cross_attestation?: CrossAttestationAnnotation
// delegation?: DelegationOutcome
// anchor_plurality?: AnchorPluralityVerdict
// 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,max_amount, andcounterpartiesare checked when the caller suppliesresolvedFactsfrom the local body or protocol event. Missing facts flagunresolvable: truerather than passing silently. 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).signatureOkrequires a valid signer entry whosecreator_keymatches the record's top-levelcreator_key; unrelated counterparty signers do not validate the record on behalf of its creator.missing: truewhen fewer than 2 distinct signer keys verify, atrib's normative minimum. Duplicate entries from one key do not inflatesigners_valid. Per §1.7.6 missing is a signal, not invalidation:validstays true if the underlying signature path holds. Agent-side Path 2 fallback records usually surface assigners_count: 1, missing: trueuntil a counterparty signs the same bytes. 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.- Trusted signer composition (D149 / §1.7.6): pass
trustedCreatorKeystoverifyRecordand the annotation additionally carriessigners_trusted(distinct verified keys that are in the trust set) andsybil_suspected(signers_valid >= 2butsigners_trusted < 2, e.g. two untrusted keys signing the same bytes).signers_validcounts verified keys, not trusted ones, so a consumer requiring non-malleable authority MUST gate on the exportedisTrustedCrossAttested(annotation)(i.e.signers_trusted >= 2), not onsigners_valid >= 2alone.trust_evaluatedis always present on transaction cross_attestation:falseis a loud signal that no trust set was supplied and only the trust-blind count was computed, so a consumer gating an action can tell the trust check was skipped.signers_trusted/sybil_suspectedare omitted until a trust set is passed, and, likemissing, are a signal that never flipsvalid.
- Trusted signer composition (D149 / §1.7.6): pass
delegation: the §1.11.4 certificate walk. PassdelegationCertificatesand, when available, the context genesis record. PreferdelegationRevocations: [{ record, log_index }]over a precomputed key set:verifyRecord()checks each revocation signature, record shape, log position, and revoker authorization before derivingrevoked.delegationRecordLogIndexpreserves historical views; omitting it computes the current view. Rejected evidence appears underdelegation.revocation_evidence, not as an accepted revocation.delegationRevokedKeysremains available for callers that already maintain a verified registry. PassdelegationTrustedTimeMswhen the result gates acceptance. Without it,in_windowuses the signed record timestamp and reportstime_basis: "record_timestamp_claim"withtime_trusted: false. Scope checks list absent amount, counterparty, trusted-time, tool, or cost facts underunverifiableand setfully_evaluated: false; missing facts no longer look like a positive scope evaluation. The result never changes signature validity.anchor_plurality: the D138 §2.11 result. Pass bothproofBundleandanchorTrust. The result reports verified, pending, malformed, and independent anchor counts, plurality and single-anchor tier facts, and cross-log hard-rejection facts. The bundle must commit to the record's canonical hash. A missing paired option, a mismatched bundle, or a hard anchor rejection adds a warning.resolveAttestationCorroboration(options)/isCorroborated(result, N)(D150 / §8.7.6): the general form of Layer 5 corroboration, lifted off transactions. An attestation is a separate signed extension-URI record (https://atrib.dev/v1/extensions/attestation) in which a signer that is NOT the target's producer vouches for a target record by reference, content{ attests: 'reliable', target, reason? }committed viaargs_hash.resolveAttestationCorroborationaggregates distinct verified attestors of a target and, reusing the D149 trust-set model, surfacesattestors_valid/attestors_trusted/under_corroborated/trust_evaluated. It counts ONLYattests: 'reliable'records (never annotation records) and rejects self-attestation, so recall-tagging cannot masquerade as trust.isCorroborated(result, N)(attestors_trusted >= N, default 2) is the guarded gate. Signal only: never flipsvalid; the fail-closed requirement lives in@atrib/action-gaterequireCorroborated.evidence: generic tiered external authorization evidence blocks (D109 / §5.5.6). Populated when the caller passesauthorizationEvidenceor when AP2 / VI evidence is mirrored into the generic shape. Each block has{ valid, protocol, issuer, subject, scope, attenuation_ok, delegation_ok, constraints, errors, warnings }. Current authorization adapters are MCP/OAuth, AAuth, and x401. These blocks do not altervalid,signatureOk, orcapability_check.ap2_vi_evidence: the async AP2 / VI verifier result (D094 / §5.5.4). Populated only when the caller passesap2ViEvidencefor a transaction record. It does not altervalid,signatureOk, orcross_attestation; consumers inspectap2_vi_evidence.validfor AP2 authorization evidence.
evaluateResultClaim(record, options): ResultClaimEvaluation
Classifies what the supplied result evidence establishes:
import { evaluateResultClaim } from '@atrib/verify'
const result = evaluateResultClaim(record, {
result: disclosedResult,
corroborations: [{ signer_key: toolKey, result_hash: record.result_hash!, verified: true }],
trusted_signer_keys: new Set([toolKey]),
min_corroborations: 1,
})status is one of:
uncommitted;committed_only;evidence_inconsistent;body_consistent_uncorroborated; orcorroborated.
The helper deduplicates trusted verified corroborators by signer key. Every
result carries truth_established: false. A signature proves who committed to
bytes. Body replay proves those bytes match the commitment. Independent
signers can corroborate the same bytes. None of those checks proves an
arbitrary real-world claim.
verifyHandoffClaims(claims, options): Promise<HandoffVerificationResult>
Verifier-side Pattern 3 handoff acceptance. Use this when one agent receives another agent's record_hash claim and needs to verify the supplied record, private body material, and proof before signing its own informed_by follow-up.
import { handoffClaimsFromEvidencePacket, verifyHandoffClaims } from '@atrib/verify'
const claims = handoffClaimsFromEvidencePacket({
required_record_hashes: [record_hash],
records: [
{
record_hash,
record,
proof,
_local: { content: body },
},
],
})
const handoff = await verifyHandoffClaims(claims, {
trusted_creator_keys: [agentAPublicKey],
allowed_context_ids: [expectedContextId],
require_body: true,
require_body_commitment: true,
require_log_inclusion: true,
log_public_key: logPublicKey,
max_age_ms: 60_000,
})
if (handoff.all_accepted) {
const informedBy = handoff.accepted_record_hashes
// Sign the receiving agent's next record with informed_by: informedBy.
}Checks performed:
- Record hash binding: supplied record hash must equal the claimed
record_hash. - Signature:
verifyRecord()must accept the signed record. - Trust set:
creator_keymust be intrusted_creator_keyswhen supplied. - Context policy:
context_idmust be inallowed_context_idswhen supplied. - Freshness: record timestamp must be within
max_age_mswhen supplied. - Body commitments: supplied
body,args, orresultmust matchargs_hash/result_hashwhen required. - Log proof: supplied proof must bind to the serialized log entry for that record, verify the inclusion path, and verify the C2SP checkpoint signature when
log_public_keyis supplied.
handoffClaimsFromEvidencePacket() accepts parsed D062 local mirror envelopes, private continuation packets, or arrays of evidence entries. It preserves missing required hashes as verifier rejections. The helper never fetches private material. Callers provide records, body material, and proof bundles from a local mirror, private handoff packet, Record Body Archive Layer, log lookup, or another channel. Rejected claims carry named reasons such as wrong_signer, wrong_context, stale, body_hash_mismatch, and proof_invalid.
The library helper backs the @atrib/verify-mcp atrib-verify primitive promoted by D106. The MCP wrapper handles agent-facing use; this package remains the verifier library.
verifyOAuthAuthorizationEvidence(evidence): Promise<OAuthAuthorizationEvidenceVerification>
Verifier-side OAuth / MCP authorization evidence checking. This runs outside the record signature path and does not fetch tokens, mint credentials, call token-introspection endpoints, or contact authorization servers. Callers supply a compact access-token JWT plus trusted JWKS, caller-verified claims, or a caller-supplied OAuth token-introspection response from a path they control. Hosts that want a library helper for the live step can call introspectOAuthToken() first, then pass its result into verifyRecord().
import { verifyRecord } from '@atrib/verify'
const result = await verifyRecord(record, {
authorizationEvidence: [
{
protocol: 'mcp_oauth',
accessTokenJwt,
jwks: [issuerJwk],
issuer: 'https://auth.example',
audience: 'https://mcp.example/mcp',
protectedResourceMetadata: {
resource: 'https://mcp.example/mcp',
authorization_servers: ['https://auth.example'],
},
requiredScopes: ['files:read'],
expectedClientId: 'https://client.example/client.json',
},
],
})For opaque access tokens, keep live network policy at the host boundary:
import {
introspectOAuthToken,
oauthEvidenceFromIntrospectionResult,
verifyRecord,
} from '@atrib/verify'
const introspection = await introspectOAuthToken({
endpoint: 'https://auth.example/oauth/introspect',
token: opaqueAccessToken,
clientAuthentication: {
method: 'basic',
clientId: process.env.OAUTH_CLIENT_ID ?? '',
clientSecret: process.env.OAUTH_CLIENT_SECRET ?? '',
},
expectedIssuer: 'https://auth.example',
expectedAudience: 'https://mcp.example/mcp',
})
const result = await verifyRecord(record, {
authorizationEvidence: [
oauthEvidenceFromIntrospectionResult(introspection, {
protocol: 'mcp_oauth',
requiredScopes: ['files:read'],
}),
],
})Checks performed when evidence is present:
- JWT signature,
iss,aud,exp,nbf, and clock-skew checks whenaccessTokenJwtandjwksare supplied. - MCP protected-resource binding through
aud, tokenresource, and protected-resource metadata. - Required OAuth scopes from
scopeorscp. - Optional RFC 9396-style
authorization_detailsconstraints bytype,actions, andlocations. - Optional
client_id, subject, actor subject, andcnf.jktchecks. - Optional DPoP proof checks for
htm,htu,ath,jti,iat, nonce when supplied, andcnf.jktbinding.
The default signature policy is require. Missing trusted keys or unverified decoded claims make the evidence block invalid. Use signaturePolicy: "best-effort" only for advisory triage. DPoP replay state stays caller-owned: pass seenJtis for one-process checks, MemoryDpopReplayCache for one-process services, or createFetchDpopReplayCache() when a deployment exposes a shared atomic replay-cache endpoint. A Cloudflare Worker and Durable Object reference for the HTTP replay-cache endpoint and host-owned introspection proxy lives at packages/integration/examples/cloudflare-agents/oauth-evidence-infra/.
import { createFetchDpopReplayCache, verifyRecord } from '@atrib/verify'
const dpopReplayCache = createFetchDpopReplayCache({
endpoint: 'https://replay-cache.example.com/v1/dpop/check',
headers: { Authorization: `Bearer ${process.env.REPLAY_CACHE_TOKEN}` },
})
const result = await verifyRecord(record, {
authorizationEvidence: [
{
protocol: 'mcp_oauth',
claims,
claimsVerified: true,
dpopProof,
dpopReplayCache,
},
],
})The shared endpoint must atomically remember the posted key until expires_at_seconds and return { "accepted": true } for a new proof or { "accepted": false } for replay. The storage backend can be Redis, Durable Objects, Postgres, or another compare-and-set primitive owned by the host.
Producer-side MCP capture lives in @atrib/mcp behind the opt-in authorizationEvidence option. The producer writes evidence into the local-only sidecar without storing raw bearer tokens by default. Verifiers can pass that sidecar's authorizationEvidence and resolvedFacts to verifyRecord().
verifyX401AuthorizationEvidence(evidence): X401AuthorizationEvidenceVerification
Verifier-side x401 proof evidence checking. This runs outside the record signature path and does not verify OpenID4VP credentials, fetch remote credential results, call issuers, exchange OAuth tokens, or mint verification tokens. Callers supply decoded x401 objects or base64url JSON header values, then pass resultVerified: true or tokenVerified: true only after their own credential or token verifier accepts the result.
import { encodeX401HeaderObject, verifyRecord } from '@atrib/verify'
const result = await verifyRecord(record, {
authorizationEvidence: [
{
protocol: 'x401',
x401: {
headers: {
'PROOF-REQUEST': encodeX401HeaderObject(proofRequest),
'PROOF-RESPONSE': encodeX401HeaderObject(resultArtifact),
},
resultVerified: true,
expectedRequestId: 'proof-template-financial-customer-v1',
expectedAgentId: 'did:web:agent.example',
expectedAgentOrigin: 'https://agent.example/origin',
agentOrigin: 'https://agent.example/origin',
agentOriginVerified: true,
issuerTrustVerified: true,
issuerTrustRootType: 'proof-trust-list',
issuerTrustRootRef: 'https://trust.example/x401.json',
proofPaymentBindingVerified: true,
proofPaymentBindingRef: 'ap2-receipt:checkout-123',
requiredSatisfiedRequirements: ['kyc:basic'],
},
},
],
})Checks performed when evidence is present:
- x401 envelope, version,
credential_requirements.digital.requests[], and OAuth token endpoint. - Request id and result-artifact binding when both are present.
- Visible OpenID4VP nonce from the credential request.
- Result artifact or x401 token object shape.
- Optional agent id, credential protocol, and satisfied requirement ids.
- Optional caller-owned agent-origin, issuer-trust, and proof-payment binding verifier facts.
PROOF-RESULTerror objects.- Payment separation: x401
paymenthints are reported as informational and do not satisfy x402, MPP, AP2, ACP, or UCP.
The default verification policy is require. A decoded proof response without resultVerified: true or tokenVerified: true is invalid, because parsing a header is not credential verification. Use verificationPolicy: "best-effort" for advisory review or "off" when another layer has already enforced the requirement. Optional agent-origin, issuer-trust, and proof-payment binding fields record caller-owned verifier outcomes. Explicit false values fail the evidence block. References are hashed in public details. Older draft names such as PROOF-REQUIRED, PROOF-PRESENTATION, and presentation_requirements are accepted with warnings by default; set allowLegacyHeaders: false or allowLegacyFields: false to reject them.
The returned details object is safe for archive projection and Explorer rendering by default. It exposes proof request, response, and result hashes, proof_gate status, payment_separation, hashed origin, trust-root, and proof-payment binding references, the visible request id, visible nonce, agent id, credential protocol, and whether a credential_result_uri was present. It does not expose raw credential payloads, raw proof-response header values, raw verification tokens, trust-list documents, proof-payment binding documents, or fetched result-by-reference bodies.
The offline x401 corpus lives at spec/conformance/5.5.6/x401/. It covers current headers, result artifacts, token responses, result-by-reference, request-id mismatch, proof-result errors, unverified proof failures, legacy-header strict mode, payment-hint separation, external origin facts, issuer-trust facts, and proof-payment binding facts.
verifyAAuthAuthorizationEvidence(evidence): Promise<AAuthAuthorizationEvidenceVerification>
Verifier-side AAuth authorization evidence checking. This runs outside the record signature path and does not fetch AAuth metadata, fetch JWKS, mint tokens, call a PS, call an AS, or perform user interaction. Callers supply a compact AAuth JWT plus trusted JWKS, caller-verified claims, or decoded claims under an explicit signaturePolicy.
import { verifyRecord } from '@atrib/verify'
const result = await verifyRecord(record, {
authorizationEvidence: [
{
protocol: 'aauth',
tokenKind: 'auth_token',
accessMode: 'auth-token',
tokenJwt: authTokenJwt,
jwks: [issuerJwk],
issuer: 'https://ps.example',
audience: 'https://api.example',
resource: 'https://api.example',
requiredScopes: ['files:read'],
expectedAgent: 'aauth:[email protected]',
expectedActSubject: 'aauth:[email protected]',
httpSignature: {
verified: true,
scheme: 'jwt',
coveredComponents: ['@method', '@authority', '@path', 'signature-key'],
signingKeyJkt,
},
},
],
})Checks performed when evidence is present:
- AAuth JWT type (
aa-agent+jwt,aa-resource+jwt, oraa-auth+jwt), signature,iss,aud,exp,iat, and clock-skew checks whentokenJwtandjwksare supplied. - Resource binding through
aud, tokenresource, and caller-suppliedaauth-resource.jsonfacts such asaccess_mode. - Required scopes from the token
scopeclaim. - Agent, subject,
parent_agent,act.sub, and mission reference checks. - HTTP Message Signature evidence: caller-verified signature status, covered components,
Authorizationcoverage forAAuth-Access, and signing-key binding throughcnf.jwk/agent_jkt. - Optional R3 document hash checks when the caller supplies R3 evidence.
The default signature policy is require. Missing trusted keys or unverified decoded claims make the evidence block invalid. Use signaturePolicy: "best-effort" or "off" only for advisory review where the caller has already made its own trust decision. The offline corpus for this adapter lives at spec/conformance/5.5.6/aauth/.
verifyAp2ViEvidence(...) and verifyAp2ViEvidenceAsync(...)
AP2 / Verifiable Intent evidence checking for merchants and auditors. This runs outside the transaction detector path. It does not alter the graph, the settlement calculation, or record validity. It answers a narrower question: did the AP2 receipts and VI mandate chain form a coherent evidence bundle?
import { verifyAp2ViEvidence, verifyAp2ViEvidenceAsync } from '@atrib/verify'
const result = verifyAp2ViEvidence({
trustedIssuerKeys: [issuerJwk],
ap2: {
paymentReceipt,
checkoutReceipt,
closedPaymentMandate,
closedCheckoutMandate,
},
vi: {
credentials: [
{ layer: 'L1', sdJwt: issuerCredential },
{ layer: 'L2', sdJwt: userMandate },
{ layer: 'L3_PAYMENT', sdJwt: agentPaymentMandate, parentPresentation },
{ layer: 'L3_CHECKOUT', sdJwt: agentCheckoutMandate, parentPresentation },
],
},
})Use the async verifier when AP2 receipts arrive as compact signed JWTs:
const result = await verifyAp2ViEvidenceAsync(
{
receiptJwtIssuers: [
{
issuer: 'https://verifier.example',
audience: 'merchant:checkout',
metadataUrl: 'https://verifier.example/.well-known/ap2',
},
],
ap2: {
paymentReceiptJwt,
closedPaymentMandate,
},
},
{ receiptJwtPolicy: 'require' },
)Checks performed when evidence is present:
- AP2 PaymentReceipt / CheckoutReceipt success, required fields, and
referencebinding to a closed mandate serialization or explicit closed-mandate hash. - Compact AP2 receipt JWT verification with
jose, using trusted local JWKS,jwksUrl, or verifier metadata containing inlinejwksorjwks_uri. - Receipt JWT hardening for unsupported
alg,alg: "none", missingkid, unexpectedcrit, malformed compact JWTs, unsupported JWKS keys, duplicatekid, metadata precedence, issuer key isolation, and clock-edge behavior. - VI SD-JWT parsing for L1, L2, L3 payment, and L3 checkout credentials.
- Async VI SD-JWT / SD-JWT VC conformance with OpenWallet
@sd-jwt/coreand@sd-jwt/sd-jwt-vc. - VI SD-JWT structural checks for duplicate disclosures, duplicate digest references, unused disclosures, unsupported
_sd_alg, and futurenbf. - ES256 signatures: L1 via trusted issuer keys, L2 via L1
cnf.jwk, L3 via the L2 delegated agent key. sd_hashlinks, disclosure digest links, autonomous L2cnf.jwkconsistency, and final checkout/payment binding.- Typed AP2 mandate constraints: merchant allowlists, checkout line items, payment amount ranges, allowed payees, allowed payment instruments, allowed PISPs, execution windows, and
payment.reference.
The default signature policy is require. Missing keys or invalid signatures make valid false while still returning a structured result. Use signaturePolicy: "best-effort" for structural triage where issuer keys are not yet available.
The default receipt JWT policy is also require. Invalid receipt JWTs make valid false. Use receiptJwtPolicy: "best-effort" when decoded receipt objects are already available and JWT verification is an advisory signal.
The async verifier also defaults sdJwtConformancePolicy to require when VI credentials are present. Each credential result includes sdJwtConformance.status, profile, and an optional reason. Use sdJwtConformancePolicy: "best-effort" for advisory conformance checks, or "off" to keep the async result aligned with the decoded structural verifier. The default profile is sd-jwt-vc; callers may pass sdJwtConformanceProfile: "sd-jwt" for the core SD-JWT profile.
VC type metadata and status-list fetches are explicit. If a caller enables sdJwtVc.loadTypeMetadata or submits credentials with VC status references, it should provide sdJwtVc.vctFetcher or sdJwtVc.statusListFetcher. The verifier does not perform hidden network fetches for those checks.
The default constraint policy is require. Failed, unresolved, or unsupported disclosed AP2 constraints make valid false. Use constraintPolicy: "best-effort" to keep those findings advisory, or "off" to skip them. The lower-level evaluateAp2ViConstraints(input, disclosures?) helper is exported for decoded mandate material and fixture replay.
Checkout line-item checks accept both atrib's decoded line_items[] fixture shape and VI checkout JWT payloads that carry purchased products under cart.items[].sku. payment.reference is evaluated against the open checkout mandate disclosure digest and the same final checkout-payment binding surfaced as checkoutPaymentBindingOk.
The local AP2 / VI evidence corpus lives under packages/agent/test/fixtures/ap2/. It includes signed immediate evidence, signed autonomous success evidence, a decoded constraint replay case, and a named autonomous negative matrix. The integration package also carries upstream-generated AP2 / VI artifacts under packages/integration/test/fixtures/ap2-vi-reference/. The crypto conformance corpus lives under spec/conformance/ap2-vi-crypto/. It pins offline JOSE, JWKS, metadata, SD-JWT, and clock-edge cases for the async verifier.
The same evidence bundle can be passed to verifyRecord(record, { ap2ViEvidence, ap2ViEvidenceOptions }) for transaction records or to verifier.verify(doc, { ap2ViEvidence, ap2ViEvidenceOptions }) for settlement recommendation verification. Both APIs attach the result as ap2_vi_evidence; neither API fetches AP2 / VI material on its own.
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.
resolveIdentity(creatorKey, options)
The §6.3 identity consultation accepts caller-pinned trust roots for the directory and the log. Step 3 reconstructs the selected directory_anchor leaf, verifies its inclusion proof against the log's signed C2SP checkpoint, and counts only fresh, valid, distinct cosignatures from trustedWitnessKeys.
Witnesses publish their own cosignatures. The log does not aggregate them. A production caller supplies fetchWitnessCosignatures, fetches each trusted witness's /v1/cosig/... response for the verified checkpoint, and returns those C2SP lines:
const result = await resolveIdentity(record.creator_key, {
directoryEndpoint: 'https://directory.atrib.dev/v6',
directoryOperatorKey,
logEndpoint: 'https://log.atrib.dev/v1',
logCheckpointKey: { name: 'log.atrib.dev/v1', publicKey: logPublicKey },
fetchAnchorBody,
previousAnchorRecordHash: verifierState.lastAcceptedDirectoryAnchorHash,
verifyAuditProof,
trustedWitnessKeys: witnesses.map(({ name, publicKey }) => ({ name, publicKey })),
witnessThreshold: 2,
fetchWitnessCosignatures: async (checkpoint) => {
const root = Buffer.from(checkpoint.rootHash).toString('base64url')
const lines = await Promise.all(
witnesses.map(async ({ endpoint }) => {
const url = `${endpoint}/v1/cosig/${encodeURIComponent(checkpoint.origin)}/${root}`
const response = await fetch(url)
if (response.status === 404) return null
if (!response.ok) throw new Error(`witness returned ${response.status}`)
return response.text()
}),
)
return lines.filter((line): line is string => line !== null)
},
})previousAnchorRecordHash is verifier state, not a value inferred from the
current log response. When supplied, step 5 follows signed chain_root links
from the selected anchor to that exact hash. It verifies every path body's
canonical hash and directory signature, strict descending log order, and log
inclusion under logCheckpointKey before calling verifyAuditProof for the
exact prior-to-current epoch range. An unrelated adjacent anchor is ignored.
After accepting the result, persist
result.anchor?.anchor_record_hash as the next consultation's prior hash.
The first consultation has no continuity baseline, so omit
previousAnchorRecordHash and expect append_only_consistent to remain
null. Supplying verifyAuditProof without the prior hash adds
step-5-previous-anchor-not-supplied; it does not make the verifier guess.
anchor_witness_count is null when the caller omits the pinned log key, the operator checkpoint or anchor inclusion proof fails verification, or configured witness retrieval fails. A value of 0 means the checkpoint and anchor inclusion verified, but the evidence presented to the verifier contained no valid trusted witness signature. A threshold shortfall adds step-3-witness-insufficient as a soft signal.
The reference directory currently returns a membership proof for its latest epoch and an unproved 404 for an absent key. resolveIdentity() rejects a membership response whose declared epoch differs from the selected anchor. Historical membership proofs and verified non-membership remain open service and AKD-bridge work.
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 verificationevaluateAp2ViConstraints(input, disclosures?): decoded AP2 open-mandate constraint checkingverifyAp2ViEvidence(bundle, options?): decoded AP2 / VI receipt and mandate-chain evidence checkingverifyAp2ViEvidenceAsync(bundle, options?): compact AP2 receipt JWT verification, async VI SD-JWT / VC conformance, plus decoded evidence checks.verifyRecord()andAtribVerifier.verify()call this when supplied withap2ViEvidenceverifyAuthorizationEvidence(evidence): generic external authorization evidence dispatch forverifyRecord()evidence blocksverifyOAuthAuthorizationEvidence(evidence): OAuth / MCP authorization evidence checks for access-token JWTs or caller-verified claimsverifyAAuthAuthorizationEvidence(evidence): AAuth authorization evidence checks for agent, resource, and auth tokens plus caller-verified HTTP signature factsverifyX401AuthorizationEvidence(evidence): x401 proof evidence checks for proof request, proof response, proof result, and caller-verified proof outcomesencodeX401HeaderObject(value)/decodeX401HeaderObject(value): base64url JSON helpers for x401 proof headersintrospectOAuthToken(options): host-owned OAuth token introspection helper for opaque-token evidenceoauthEvidenceFromIntrospectionResult(result, base?): adapter from host-owned introspection result to OAuth evidence inputcreateFetchDpopReplayCache(options): HTTP-backed DPoP replay-cache adapter for fleet-shared replay checksMemoryDpopReplayCache: in-process implementation of theDpopReplayCachecontract for tests and single-worker deploymentsverifyHandoffClaims(claims, options?): Pattern 3 handoff claim acceptance before a receiving agent signs aninformed_byfollow-upverifyOperatorCheckpoint(note, trustedKey): parse and verify one C2SP checkpoint against an operator key pinned by the callercreateWitnessCosignature(options): create the normative timestamped 76-byte C2SP witness cosignatureverifyCheckpointWitnessThreshold(note, policy): verify the operator signature, trusted witness cosignatures, freshness, distinct-key count, and caller-selected thresholdverifyCheckpointConsistencyFromLeafHashes(previous, current, leafHashes): recompute prior and current RFC 6962 roots from a witness-held leaf history before accepting an extensionanalyzeCheckpointGossip(observations, operatorKey): verify independently observed checkpoints, compare equal-size roots or complete leaf-hash prefixes, detect source rollback, and return deterministic split-view incident objects. Different-size checkpoints without prefix evidence areinconclusive, never silently consistentrecommendationSigningInput(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 logs anatrib: ...warning, setsmerchantPrivateKey = null, and does not 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
The test suite covers 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, AP2 / VI evidence checking, AP2 / VI crypto conformance, x401 proof evidence checking, x401 authorization evidence conformance, tiered AP2 / VI verifier attachment, checkpoint and witness signature verification, witness thresholds and freshness, 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) |
| Payments profile §5 | Default policy document (relocated from spec §4.3 per D147) |
| Payments profile §8 | Pure calculation algorithm (relocated from spec §4.6) |
| Payments profile §9 | Recommendation document signing/verification (relocated from spec §4.7) |
| §5.5 | AtribVerifier class. verify() and calculate() |
| §5.5.4 | AP2 / VI evidence checks |
| §5.8 | Degradation contract; failures never break the host |
| §2.9 | Pinned-key checkpoint and witness cosignature verification |
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.services/witness-node, the external-operator-ready checkpoint witness reference servicepackages/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.
Part of atrib
atrib is an open protocol for verifiable agent actions. Every action becomes a signed, chain-linked record that anyone can verify against a public Merkle log, with no operator to trust. This package is one entrypoint. See the full package family and the protocol spec.
