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
| Module | What it does |
|---|---|
atproto_core/xrpc | TransportError, XrpcError, describe, parse, check_ok |
atproto_core/remote | Origin/Url: https-only, public-host parsing of remote URLs |
atproto_core/did | Did and native DID document resolution (PLC, did:web) |
atproto_core/identity | Slingshot resolveMiniDoc (a hint, never trusted for auth) |
atproto_core/oauth/effect | The Effect type: Fetch/FetchBits/DpopProof/Done steps |
atproto_core/oauth/transport | Form POST with DPoP, client auth, and the use_dpop_nonce retry |
atproto_core/oauth/metadata | Verified protected-resource and authorization-server metadata |
atproto_core/oauth/flow | start (resolve, discover, PAR) and callback (verify, exchange) |
atproto_core/oauth/session | VerifiedSession: tokens bound to a verified DID and issuer |
atproto_core/oauth/tokens | Refresh and revocation of a VerifiedSession |
atproto_core/oauth/resource | DPoP-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:
- 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:webhost) must list the handle inalsoKnownAs. - The PDS is the DID document’s
#atproto_pdsservice, parsed as aremote.Origin. - The PDS’s protected-resource metadata must name that PDS as
resource. - The authorization server’s metadata
issuermust be exactly the issuer it was fetched from, and it must advertise the atproto profile capabilities. flow.callbackrequiresstateandiss, then exchanges the code. The tokensubmust be the flow’s DID,token_typeDPoP, the scope must includeatproto, and the DID must resolve back to the same issuer.- 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.