Skip to main content
This page covers the default agent flow. Per-symbol typedoc reference is planned but not yet published.

Two responsibilities

  • AgentAgent.generate() / Agent.fromPrivateKey(), signRequest, the protocol-aware builders (owner invitation, key rotation, account introspection), and fetchDiscovery / assertDiscoveryDocument.
  • TrustClient (AFAP-0006) — binds the agent’s DID to a human and mints the attestation that default services require.

Default flow: signing up to an attested_only service

Services built with defineService default to unclaimed_mode: "attested_only", so a request with no AFAuth-Attestation header is rejected with 401 attestation_required. The agent links to a human once, then mints a per-service JWT per request:
import { Agent, TrustClient, fetchDiscovery } from "@afauthhq/agent";

const agent = await Agent.generate();
const trust = new TrustClient({
  agentDid:        agent.did,
  agentPublicKey:  agent.publicKey,
  agentPrivateKey: agent.exportPrivateKey(),
});

// One-time: link the agent's DID to a human.
const { req_id, link_url } = await trust.linkStart({ label: "my agent" });
console.log(`Have a human confirm: ${link_url}`);
let binding;
while (!(binding = await trust.linkPoll(req_id))) {
  await new Promise((r) => setTimeout(r, 2_000)); // persist binding for reuse
}

// Per request: mint an audience-bound attestation and present it.
const disc = await fetchDiscovery("https://api.example.com");
const { jwt } = await trust.token(disc.service_did);

const signed = await agent.buildAccountIntrospection({ baseUrl: "https://api.example.com" });
await fetch(signed.url, {
  method: signed.method,
  headers: { ...signed.headers, "AFAuth-Attestation": jwt },
});
Against a unclaimed_mode: "free" service the link/mint steps are unnecessary — a bare signed request suffices. Full walkthrough: Link your agent to a human and the Trust API reference.

One call: signup() + the shared agent home

signup() collapses the flow above into one call — discover, link to a human only if the service is attested_only and the agent isn’t linked yet, then send the signed implicit-signup request:
import { signup } from "@afauthhq/agent";

const result = await signup({
  agent,
  baseUrl: "https://api.example.com",
  onLink: (url) => console.error(`Approve once: ${url}`), // fires only under attested_only
});
// → { account, discovery, binding } — persist result.binding for reuse
It’s mode-adaptive: against an optional service onLink never fires and the agent signs up with just its key. This is the call a service-distributed CLI builds on. For Node clients, @afauthhq/agent/node persists the shared agent home (~/.afauth, honouring $AFAUTH_HOME) so a human links once across every AFAuth tool on the machine — in the same on-disk format the afauth CLI uses:
  • loadOrCreateAgent() / loadAgent / saveAgent — the did:key keypair (key.json).
  • loadBinding / saveBinding — trust-attestor bindings (trust.json).
import { loadOrCreateAgent } from "@afauthhq/agent/node";

const { agent } = await loadOrCreateAgent(); // reuse the machine's identity, or create one

Keeping access live (§10.7)

For ongoing requests to an attested-session service, AttestedFetcher runs the refresh-on-challenge loop for you — it signs each request and, on 401 attestation_required, mints a fresh attestation (via TrustClient) and retries once:
import { Agent, TrustClient, AttestedFetcher } from "@afauthhq/agent";

const fetcher = new AttestedFetcher({ agent, trust, serviceDid: disc.service_did });
const res = await fetcher.fetch({ method: "GET", url: "https://api.example.com/api/resource" });
A revoked/expired binding surfaces as a terminal TrustHttpError (re-link, don’t retry). See Keep attested access live.