atproto/browser/oauth/flow

Async mirror of atproto/oauth/flow.gleam: start resolves the account natively, discovers and verifies its authorization server, and pushes the request (PAR) with PKCE and DPoP; callback checks the redirect (state, iss, error), exchanges the code, and yields a session whose sub was verified against the issuer.

A thin async wrapper over atproto_core/oauth/flow: it generates the browser one-shots (WebCrypto PKCE, WebCrypto DPoP key, state) and pairs the kernel’s pending flow with the key the kernel never sees. Meant for a public client.

Surviving the redirect: a Flow is not plain data. Its DpopKey is a non-extractable WebCrypto CryptoKeyPair, which cannot be written to sessionStorage or JSON at all, and its Gleam values do not survive a structured clone. Store the two halves separately before navigating: to_json(flow) in sessionStorage under state(flow), and dpop_key(flow) in IndexedDB (structured clone keeps a CryptoKeyPair usable and still non-extractable). On the callback page, take (read and delete) both by the returned state and resume before callback. A flow is single-use; deleting the stored halves is what enforces that.

Types

A completed login: the verified session and the DPoP key its tokens are bound to. Keep the key in IndexedDB alongside the session.

pub type Authenticated {
  Authenticated(
    session: session.VerifiedSession,
    dpop_key: dpop.DpopKey,
  )
}

Constructors

pub type CallbackError =
  flow.CallbackError
pub type ClientAuth =
  transport.ClientAuth

Flow

opaque
pub opaque type Flow
pub type FlowError =
  flow.StartError
pub type ResumeError =
  flow.ResumeError

Values

pub fn callback(
  client: xrpc.Client,
  flow flow: Flow,
  query query: List(#(String, String)),
  client_auth client_auth: transport.ClientAuth,
) -> promise.Promise(Result(Authenticated, flow.CallbackError))

Complete the flow from the redirect’s query parameters, all of them as received (e.g. uri.parse_query of window.location.search).

pub fn callback_error_to_string(
  error: flow.CallbackError,
) -> String
pub fn did(flow: Flow) -> did.Did

The account the flow was started for.

pub fn dpop_key(flow: Flow) -> dpop.DpopKey

The flow’s key half, for IndexedDB. See the module docs.

pub fn flow_error_to_string(error: flow.StartError) -> String
pub fn issuer(flow: Flow) -> remote.Origin
pub fn pds(flow: Flow) -> remote.Origin
pub fn resume(
  client: xrpc.Client,
  stored stored: String,
  dpop_key dpop_key: dpop.DpopKey,
) -> promise.Promise(Result(Flow, flow.ResumeError))

Rebuild a flow from to_json output and its stored DPoP key, re-verifying the issuer’s metadata.

pub fn resume_error_to_string(error: flow.ResumeError) -> String
pub fn resume_with(
  client: xrpc.Client,
  stored stored: String,
  dpop_key dpop_key: dpop.DpopKey,
  policy policy: remote.Policy,
  plc_directory plc_directory: remote.Origin,
) -> promise.Promise(Result(Flow, flow.ResumeError))

resume for a flow started with start_with: the stored origins are parsed under policy, and plc_directory must be the one the flow was started with. Local development only.

pub fn start(
  client: xrpc.Client,
  resolver resolver: String,
  identifier identifier: String,
  client_id client_id: String,
  redirect_uri redirect_uri: String,
  scope scope: String,
  client_auth client_auth: transport.ClientAuth,
) -> promise.Promise(Result(#(String, Flow), flow.StartError))

Returns the authorization-server URL to send the user to, and the pending flow to keep until the callback.

pub fn start_with(
  client: xrpc.Client,
  resolver resolver: String,
  identifier identifier: String,
  client_id client_id: String,
  redirect_uri redirect_uri: String,
  scope scope: String,
  client_auth client_auth: transport.ClientAuth,
  plc_directory plc_directory: remote.Origin,
) -> promise.Promise(Result(#(String, Flow), flow.StartError))

start with DID documents taken from plc_directory, for local development against a devnet. Parse the directory with remote.parse_origin_with(_, remote.AllowLocalDevelopment); its policy then governs the whole flow, and the resulting Flow, session, refresh and revoke inherit it. Never use this in production.

pub fn state(flow: Flow) -> String

The state this flow was started with: the key to store it under.

pub fn to_json(flow: Flow) -> String

The flow’s plain half as JSON, for sessionStorage. See the module docs.

✨ Search Document