Verify a credential offline with the SDK
Task: take an AID an agent presented (or fetched from the API) and decide whether it is valid, without calling the registry at decision time.
1 · Install
npm install @aria-registry/verify@1.1.0The package pins the issuer's public keys, so verification is local. It pulls @noble/post-quantum, @noble/curves and @noble/hashes; expect about 5 MB installed.
2 · Get a credential
Any resolvable DID works. Replace <did> with one you were given — your own from the portal, for instance.
curl -s https://api.aria.bar/v1/aids/<did> > credential.json3 · Verify
import { verifyAgent } from '@aria-registry/verify';
// `credential` is the AID JSON the agent presented, or the document at
// https://api.aria.bar/v1/aids/<did>. Verification runs offline.
const result = await verifyAgent(credential);
if (result.valid) {
result.did; // "did:aria:example.com:my-agent"
result.trustLevel; // "L0" today; L1–L3 once issued
result.scopes; // ["commerce:order:create", …]
result.credentialId; // credential-instance URL
result.principal
.verificationStatus; // "self-declared" | "registry-confirmed"
// | "legal-verified" | null
result.revocationStatus; // 'unknown' — verifyAgent never checks revocation.
// Use checkRevocation(did), or a policy that requires it.
}Both signature halves must pass: a flipped byte in the ML-DSA-65 half fails with signatures.pqValid === false, one in the Ed25519 half with classicalValid === false. This was checked against a live credential on 2026-09-07 (ATP/1 vector 11).
4 · Decide what revocation means to you
verifyAgent() does not check revocation. It returns revocationStatus: 'unknown' and the credential passes. This is deliberate — offline verification is the point — but an integrator who reads valid and stops will accept a revoked credential.
Two things turn revocation into part of the verdict. requireRevocationCheck makes it required at all; maxOfflineAge bounds how old the evidence may be. Never pass null, which means "any age"; one hour is a reasonable default and the protocol floor is 60 seconds (§8, fresh=).
const status = await checkRevocation(did); // networked, per DID
const result = await verifyAgent(credential, {
policy: {
requireRevocationCheck: true,
lastRevocationCheck: Date.parse(status.checkedAt),
maxOfflineAge: 60 * 60 * 1000,
},
});checkRevocation asks /v1/verify/{did}, which tells the registry which agent you are asking about. Reading the aggregate Bitstring Status List yourself avoids that, and moving the SDK to it is planned.
You should see
For a valid L0 credential:
result.valid // true
result.trustLevel // "L0"
result.signatures // { pqValid: true, classicalValid: true, suite: "mldsa65-ed25519-2026" }
result.principal.verificationStatus // "self-declared"
result.revocationStatus // "unknown" — until a policy requires the checkWhat you have now is identification: the credential is genuine and current. You have not proven that the caller holds its key — that is the holder proof under ATP, in deployment (/planned#challenge-endpoint). Read What an AID proves before gating anything on this result.