atproto_core

The shared sans-io core of the gleam-atproto client packages: the XRPC error vocabulary and response handling, plus the OAuth effect kernel that atproto_client (sync, erlang) and atproto_browser (async, javascript) both interpret with their own runners.

This package performs no IO and depends on no HTTP backend. Unless you are building a runner for a new target, you want atproto_client or atproto_browser instead; they re-surface everything here behind their own entry points.

Modules

ModuleWhat it does
atproto_core/xrpcTransportError, XrpcError, describe, parse, check_ok
atproto_core/remoteOrigin/Url: https-only, public-host parsing of remote URLs
atproto_core/didDid and native DID document resolution (PLC, did:web)
atproto_core/identitySlingshot resolveMiniDoc (a hint, never trusted for auth)
atproto_core/oauth/effectThe Effect type: Fetch/FetchBits/DpopProof/Done steps
atproto_core/oauth/transportForm POST with DPoP, client auth, and the use_dpop_nonce retry
atproto_core/oauth/metadataVerified protected-resource and authorization-server metadata
atproto_core/oauth/flowstart (resolve, discover, PAR) and callback (verify, exchange)
atproto_core/oauth/sessionVerifiedSession: tokens bound to a verified DID and issuer
atproto_core/oauth/tokensRefresh and revocation of a VerifiedSession
atproto_core/oauth/resourceDPoP-signed requests, pinned to the session’s PDS origin

The effect kernel in one paragraph

Every OAuth operation is a pure Effect value: a step asking for a fetch, a bit-array fetch, or a DPoP proof, with a continuation for the answer. A runner walks the value, performing real IO per step; the kernel never sees a socket or a key. That is what lets one implementation of the nonce-retry and token-exchange logic serve both a sync erlang client and an async browser client, and it is why the whole kernel is unit-testable by stepping effects by hand (see test/oauth_effect_test.gleam).

What the OAuth kernel verifies

Every hop of discovery is attacker-influenced, so each is parsed and checked before the next request is made:

  1. The account is a DID. A handle goes through the resolver for its DID only; the DID document (fetched natively from the PLC directory or the did:web host) must list the handle in alsoKnownAs.
  2. The PDS is the DID document’s #atproto_pds service, parsed as a remote.Origin.
  3. The PDS’s protected-resource metadata must name that PDS as resource.
  4. The authorization server’s metadata issuer must be exactly the issuer it was fetched from, and it must advertise the atproto profile capabilities.
  5. flow.callback requires state and iss, then exchanges the code. The token sub must be the flow’s DID, token_type DPoP, the scope must include atproto, and the DID must resolve back to the same issuer.
  6. The access token is only ever sent to the session’s PDS origin.

What parsing cannot decide is the transport’s job: no redirects, bounded response size and request time, and on a server, no connections to private, loopback, link-local, or unique-local addresses after DNS resolution.

✨ Search Document