verify

@atiproto/atproto-attestation

Standalone verifier. Walks a record's signatures[] and checks each entry. Returns a structured result rather than throwing, so callers decide whether a partial pass is acceptable.

Signature

typescript
verify(input: VerifyInput): Promise<VerifyResult>
interface VerifyInput {
record: RecordMap;
repository: string; // DID that owns the record
fields?: readonly string[]; // must match what the signer used
role?: string; // filter to a specific role
keyResolver?: KeyResolver; // defaults to did:key parsing
recordResolver?: RecordResolver; // required for remote entries
}
interface VerifyResult {
valid: boolean; // true iff every checked entry verified
entries: VerifyEntryResult[];
}
interface VerifyEntryResult {
index: number;
$type: string;
ok: boolean;
reason?: string; // populated when ok is false
}

What is verified

  • For an inline entry: the CID is recomputed over the record + the entry's own metadata + the supplied repository, and the attached ECDSA / Ed25519 signature is verified against the public key resolved from entry.key.
  • For a remote entry (com.atproto.repo.strongRef): the proof record is fetched via recordResolver, the CID is recomputed, and proof.cid must equal it. Authority comes from the repo hosting the proof, so there are no signature bytes to check.
  • When role is supplied, entries whose role does not match are skipped, not failed. Inline entries are matched on entry.role; remote entries on proof.role.
  • valid is false when the after-filter set is empty.

Examples

Inline only

typescript
import { verify } from "@atiproto/atproto-attestation";
const result = await verify({
record: cart,
repository: "did:plc:recipient",
fields: ["items", "currency", "status"],
});
if (!result.valid) {
for (const e of result.entries) {
if (!e.ok) console.warn(`#${e.index} (${e.$type}): ${e.reason}`);
}
}

Require an AppView signature

typescript
const appViewOk = await verify({
record: cart,
repository: "did:plc:recipient",
fields: ["items", "currency", "status"],
role: "appview",
});
if (!appViewOk.valid) throw new Error("missing or bad AppView attestation");

Inline + remote, with resolvers

typescript
import { verify } from "@atiproto/atproto-attestation";
import { didDocKeyResolver, fetchProofRecord } from "./resolvers";
const result = await verify({
record: cart,
repository: "did:plc:recipient",
fields: ["items", "currency", "status"],
keyResolver: didDocKeyResolver,
recordResolver: fetchProofRecord,
});

See KeyResolver & RecordResolver for the resolver shapes and worked implementations.

Why a structured result

The verifier reports per-entry rather than throwing because real records carry multiple attestations (PoS, AppView, broker) and partial validity is often what callers want, e.g. accept a cart whose PoS signature is good, even if a stale AppView signature is left over. Callers can collapse the result to a boolean via result.valid when strictness is needed.