KeyResolver & RecordResolver
@atiproto/atproto-attestation
Two pluggable functions used by verify. They fetch verification material that lives outside the record. Ready-made implementations ship in the @atiproto/key-resolver and @atiproto/record-resolver packages.
KeyResolver
type KeyResolver = (keyRef: string) => Promise<KeyData> | KeyData;interface KeyData {type: "p256" | "p384" | "k256" | "ed25519";bytes: Uint8Array;}
Called for every inline signature entry. The argument is the string stored at signature.key, typically a did:key:… but in principle anything your protocol uses (DID + key id, JWK URL, etc).
The verifier's default resolver only understands bare did:key: strings. Use one of the prebuilt factories in @atiproto/key-resolver to verify against keys hosted in a DID document.
Prebuilt KeyResolvers
- createDidKeyResolver: local-only parser for
did:key:references. - createFetchKeyResolver: fetches the DID document from the PLC directory or did:web host.
- createCachedKeyResolver: caches the fetched DID document, suitable for repeated verification on edge runtimes.
import { verify } from "@atiproto/atproto-attestation";import { createCachedKeyResolver } from "@atiproto/key-resolver";const keyResolver = createCachedKeyResolver();await verify({record,repository,fields,keyResolver,});
RecordResolver
type RecordResolver = (uri: string) => Promise<RecordMap> | RecordMap;type RecordMap = { [key: string]: unknown };
Called for every strongRef entry on signatures[]. Given an at:// URI, return the proof record stored at that location. The returned record must include its $type (typically network.attested.proof) and the cid field that the verifier compares against the recomputed canonical CID.
Without a recordResolver, remote attestations fail with "Remote attestation requires input.recordResolver".
Prebuilt RecordResolvers
- createFetchRecordResolver: plain fetch against a configurable relay or PDS.
- createAgentRecordResolver: routes
getRecordthrough an existing XRPC-shaped client. - createCachedRecordResolver: cached fetch. Safe because proof records are content-addressed and never mutate in place.
import { verify } from "@atiproto/atproto-attestation";import { createCachedRecordResolver } from "@atiproto/record-resolver";const recordResolver = createCachedRecordResolver();await verify({record,repository,fields,recordResolver,});
Full example
import { verify } from "@atiproto/atproto-attestation";import { createCachedKeyResolver } from "@atiproto/key-resolver";import { createCachedRecordResolver } from "@atiproto/record-resolver";const keyResolver = createCachedKeyResolver();const recordResolver = createCachedRecordResolver();const result = await verify({record: cart,repository: "did:plc:recipient",fields: ["items", "currency", "status"],keyResolver,recordResolver,});
Custom resolvers
Both types are plain functions. If the prebuilt factories don't fit (e.g. you need a custom key reference format, or your proof records are mirrored to a non-XRPC store), write your own. Any function matching the shape above works.