npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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/verify

Verify 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)

  1. Resolves the calculator's public key from recommendationDoc.calculated_by. For the well-known resolve.atrib.dev service, the key is fetched from the /pubkey endpoint. For other calculators, the merchant supplies the key out-of-band.
  2. Verifies the Ed25519 signature over the JCS-canonicalized recommendation document (excluding the signature field).
  3. Fetches the attribution graph at recommendationDoc.graph_tree_size from the configured graph endpoint. Pinning to a specific tree size makes the verification reproducible; the graph is fixed, not "live."
  4. Fetches the session policy record referenced by policy_record_id, or uses the spec §4.3 default policy if policy_record_id === 'default'.
  5. Re-runs calculate(graph, policy, sessionPolicyRecord) locally; a pure function with no network calls and no randomness.
  6. Compares distributions using distributionsMatch() (within 1e-9 per 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 against

Per 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 carrying provenance_token (D044 / §1.2.6). The 16-byte token truncation is irreversible: upstream_record_hash populates 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 carrying informed_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-detected args_hash / result_hash commitment scheme: 'salted-sha256' when args_salt / result_salt is 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.2 tool_name_form: 'hashed' when tool_name matches ^sha256:[0-9a-f]{64}$, 'plain' for any other present value, null when 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 resolved identityClaim in options. Checks the record's event_type against the envelope's event_types allowlist and the record's timestamp against expires_at. tool_names, max_amount, and counterparties are checked when the caller supplies resolvedFacts from the local body or protocol event. Missing facts flag unresolvable: true rather than passing silently. Per §6.7.3 out-of-envelope is a signal, not invalidation: mismatches do not flip valid to false. The caller is responsible for fetching the active envelope at the record's timestamp via @atrib/directory's lookup() (or a cached equivalent); @atrib/verify intentionally has no @atrib/directory dependency.

  • cross_attestation: { signers_count, signers_valid, missing } (D052 / §1.7.6). Populated only on transaction records (event_type = transaction). Each entry in signers[] is verified against the cross-attestation canonical bytes (JCS form with signers: [] and the top-level signature field omitted, per §1.7.6). signatureOk requires a valid signer entry whose creator_key matches the record's top-level creator_key; unrelated counterparty signers do not validate the record on behalf of its creator. missing: true when fewer than 2 distinct signer keys verify, atrib's normative minimum. Duplicate entries from one key do not inflate signers_valid. Per §1.7.6 missing is a signal, not invalidation: valid stays true if the underlying signature path holds. Agent-side Path 2 fallback records usually surface as signers_count: 1, missing: true until a counterparty signs the same bytes. Legacy single-signer transaction records (no signers[] array, only top-level signature) surface as signers_count: 0, missing: true so consumers can flag them while accepting the cryptographic validity.

    • Trusted signer composition (D149 / §1.7.6): pass trustedCreatorKeys to verifyRecord and the annotation additionally carries signers_trusted (distinct verified keys that are in the trust set) and sybil_suspected (signers_valid >= 2 but signers_trusted < 2, e.g. two untrusted keys signing the same bytes). signers_valid counts verified keys, not trusted ones, so a consumer requiring non-malleable authority MUST gate on the exported isTrustedCrossAttested(annotation) (i.e. signers_trusted >= 2), not on signers_valid >= 2 alone. trust_evaluated is always present on transaction cross_attestation: false is 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_suspected are omitted until a trust set is passed, and, like missing, are a signal that never flips valid.
  • delegation: the §1.11.4 certificate walk. Pass delegationCertificates and, when available, the context genesis record. Prefer delegationRevocations: [{ record, log_index }] over a precomputed key set: verifyRecord() checks each revocation signature, record shape, log position, and revoker authorization before deriving revoked. delegationRecordLogIndex preserves historical views; omitting it computes the current view. Rejected evidence appears under delegation.revocation_evidence, not as an accepted revocation. delegationRevokedKeys remains available for callers that already maintain a verified registry. Pass delegationTrustedTimeMs when the result gates acceptance. Without it, in_window uses the signed record timestamp and reports time_basis: "record_timestamp_claim" with time_trusted: false. Scope checks list absent amount, counterparty, trusted-time, tool, or cost facts under unverifiable and set fully_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 both proofBundle and anchorTrust. 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 via args_hash. resolveAttestationCorroboration aggregates distinct verified attestors of a target and, reusing the D149 trust-set model, surfaces attestors_valid / attestors_trusted / under_corroborated / trust_evaluated. It counts ONLY attests: '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 flips valid; the fail-closed requirement lives in @atrib/action-gate requireCorroborated.

  • evidence: generic tiered external authorization evidence blocks (D109 / §5.5.6). Populated when the caller passes authorizationEvidence or 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 alter valid, signatureOk, or capability_check.

  • ap2_vi_evidence: the async AP2 / VI verifier result (D094 / §5.5.4). Populated only when the caller passes ap2ViEvidence for a transaction record. It does not alter valid, signatureOk, or cross_attestation; consumers inspect ap2_vi_evidence.valid for 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; or
  • corroborated.

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_key must be in trusted_creator_keys when supplied.
  • Context policy: context_id must be in allowed_context_ids when supplied.
  • Freshness: record timestamp must be within max_age_ms when supplied.
  • Body commitments: supplied body, args, or result must match args_hash / result_hash when 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_key is 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 when accessTokenJwt and jwks are supplied.
  • MCP protected-resource binding through aud, token resource, and protected-resource metadata.
  • Required OAuth scopes from scope or scp.
  • Optional RFC 9396-style authorization_details constraints by type, actions, and locations.
  • Optional client_id, subject, actor subject, and cnf.jkt checks.
  • Optional DPoP proof checks for htm, htu, ath, jti, iat, nonce when supplied, and cnf.jkt binding.

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-RESULT error objects.
  • Payment separation: x401 payment hints 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, or aa-auth+jwt), signature, iss, aud, exp, iat, and clock-skew checks when tokenJwt and jwks are supplied.
  • Resource binding through aud, token resource, and caller-supplied aauth-resource.json facts such as access_mode.
  • Required scopes from the token scope claim.
  • Agent, subject, parent_agent, act.sub, and mission reference checks.
  • HTTP Message Signature evidence: caller-verified signature status, covered components, Authorization coverage for AAuth-Access, and signing-key binding through cnf.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 reference binding 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 inline jwks or jwks_uri.
  • Receipt JWT hardening for unsupported alg, alg: "none", missing kid, unexpected crit, malformed compact JWTs, unsupported JWKS keys, duplicate kid, 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/core and @sd-jwt/sd-jwt-vc.
  • VI SD-JWT structural checks for duplicate disclosures, duplicate digest references, unused disclosures, unsupported _sd_alg, and future nbf.
  • ES256 signatures: L1 via trusted issuer keys, L2 via L1 cnf.jwk, L3 via the L2 delegated agent key.
  • sd_hash links, disclosure digest links, autonomous L2 cnf.jwk consistency, 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 function
  • DEFAULT_POLICY: the spec §4.3 default policy document
  • isValidPolicy(doc): schema check for PolicyDocument
  • signRecommendation(unsigned, privateKey): JCS + Ed25519 signing
  • verifyRecommendationSignature(doc, publicKey): signature verification
  • evaluateAp2ViConstraints(input, disclosures?): decoded AP2 open-mandate constraint checking
  • verifyAp2ViEvidence(bundle, options?): decoded AP2 / VI receipt and mandate-chain evidence checking
  • verifyAp2ViEvidenceAsync(bundle, options?): compact AP2 receipt JWT verification, async VI SD-JWT / VC conformance, plus decoded evidence checks. verifyRecord() and AtribVerifier.verify() call this when supplied with ap2ViEvidence
  • verifyAuthorizationEvidence(evidence): generic external authorization evidence dispatch for verifyRecord() evidence blocks
  • verifyOAuthAuthorizationEvidence(evidence): OAuth / MCP authorization evidence checks for access-token JWTs or caller-verified claims
  • verifyAAuthAuthorizationEvidence(evidence): AAuth authorization evidence checks for agent, resource, and auth tokens plus caller-verified HTTP signature facts
  • verifyX401AuthorizationEvidence(evidence): x401 proof evidence checks for proof request, proof response, proof result, and caller-verified proof outcomes
  • encodeX401HeaderObject(value) / decodeX401HeaderObject(value): base64url JSON helpers for x401 proof headers
  • introspectOAuthToken(options): host-owned OAuth token introspection helper for opaque-token evidence
  • oauthEvidenceFromIntrospectionResult(result, base?): adapter from host-owned introspection result to OAuth evidence input
  • createFetchDpopReplayCache(options): HTTP-backed DPoP replay-cache adapter for fleet-shared replay checks
  • MemoryDpopReplayCache: in-process implementation of the DpopReplayCache contract for tests and single-worker deployments
  • verifyHandoffClaims(claims, options?): Pattern 3 handoff claim acceptance before a receiving agent signs an informed_by follow-up
  • verifyOperatorCheckpoint(note, trustedKey): parse and verify one C2SP checkpoint against an operator key pinned by the caller
  • createWitnessCosignature(options): create the normative timestamped 76-byte C2SP witness cosignature
  • verifyCheckpointWitnessThreshold(note, policy): verify the operator signature, trusted witness cosignatures, freshness, distinct-key count, and caller-selected threshold
  • verifyCheckpointConsistencyFromLeafHashes(previous, current, leafHashes): recompute prior and current RFC 6962 roots from a witness-held leaf history before accepting an extension
  • analyzeCheckpointGossip(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 are inconclusive, never silently consistent
  • recommendationSigningInput(doc): the canonical bytes that get signed
  • distributionsMatch(a, b): float-tolerant equality (within 1e-9 per 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-9 for 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 an atrib: ... warning, sets merchantPrivateKey = null, and does not throw.
  • verify() errors during signature resolution, graph fetch, or calculation are caught and surfaced as warnings: string[] with valid = 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 records verify() 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 service
  • packages/integration/examples/end-to-end/, runnable demo wiring everything together
  • DECISIONS.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 at github.com/creatornader/atrib/blob/main/... URLs that will resolve once the repository goes public. Until then, see atrib.dev for 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.