Packages
A browser-native atproto OAuth + XRPC client for Gleam: WebCrypto DPoP, PKCE, and async transport, for building a public-client SPA with no server-side session custody.
Current section
Files
Jump to
Current section
Files
atproto_browser
README.md
README.md
# atproto_browser
> **Alpha, pre-release.** Proven via the manual smoke scripts below, not yet
> used in a real browser tab or backed by persistent key storage. Expect
> breaking changes.
A browser-native [atproto](https://atproto.com/) OAuth + XRPC client for
Gleam: WebCrypto DPoP and PKCE, async `fetch` transport, and the full
PAR/token-exchange/authenticated-request flow for a public client. Separate
from [`atproto_client`](https://hex.pm/packages/atproto_client) because real browser `fetch` is
async and DPoP/PKCE need browser WebCrypto; this package is
javascript-target-only and owns the `atproto/browser/*` namespace.
## Installation
JavaScript target only:
```sh
gleam add atproto_browser
```
## Usage
```gleam
import atproto/browser/oauth/flow
import atproto/browser/xrpc
import atproto_core/oauth/transport
// handle -> DID -> DID document -> PDS -> verified auth server -> PAR;
// returns the authorization URL to redirect the user to, plus the Flow.
flow.start(
xrpc.fetch_client(),
resolver: "https://slingshot.microcosm.blue",
identifier: handle,
client_id: client_id,
redirect_uri: redirect_uri,
scope: "atproto transition:generic",
client_auth: transport.PublicClient,
)
```
Before redirecting, persist the flow in two halves: `flow.to_json(pending)`
in `sessionStorage` under `flow.state(pending)`, and `flow.dpop_key(pending)`
in IndexedDB. The key is a non-extractable WebCrypto `CryptoKeyPair`: it
cannot go into `sessionStorage` or JSON, but IndexedDB's structured clone
keeps it usable and still non-extractable. On the callback page, take (read
and delete) both by the returned `state`, `flow.resume` them, and hand the
whole query to `flow.callback`. It checks `state` and `iss`, exchanges the
code, and returns `Authenticated(session, dpop_key)` only when the token's
`sub` is the DID the flow started for and resolves back to the same
authorization server. Wrap a client with `oauth/resource` so requests to the
session's PDS (and only there) are DPoP-signed.
`xrpc.fetch_client()` refuses redirects (`redirect: "error"`), so a 3xx on
any OAuth hop fails instead of landing on a host nobody validated.
## Architecture
| Module | What it does |
| -------------------------------- | --------------------------------------------------------------------------- |
| `atproto/browser/xrpc` | Async `Client`, `XrpcError`, `get`/`post_json`/etc., `fetch_client()` |
| `atproto/browser/dpop` | WebCrypto DPoP (RFC 9449) key generation + proof signing |
| `atproto/browser/identity` | `resolve_pds` via Slingshot `resolveMiniDoc` (a hint, not trusted for auth) |
| `atproto/browser/oauth/flow` | `start`, `callback`, and `to_json`/`resume` across the redirect |
| `atproto/browser/oauth/tokens` | `refresh`/`revoke` of a `VerifiedSession` |
| `atproto/browser/oauth/resource` | Wraps a `Client` so requests to the session's PDS carry the DPoP token |
The rest (`pkce`, `webcrypto`, `oauth/runner`) is internal plumbing behind
these six: PKCE and WebCrypto primitives and the effect-kernel runner that
`flow`, `tokens`, and `resource` interpret against. The package does not
depend on `gleam_crypto`, whose JavaScript FFI imports `node:crypto`.
## Development
```sh
gleam test # pure, synchronous pieces
node test/manual/dpop_smoke.ts # real WebCrypto DPoP sign + verify
node test/manual/oauth_flow_smoke.ts # full flow, attacks, redirect round trip
```
The smoke tests run the actual compiled output against real WebCrypto and a
fake-but-realistic PDS/authorization server, parameterized over proof shapes
and server behaviors (including the `use_dpop_nonce` retry); gleeunit has no
Promise-test support, so they are the regression check for the async paths
and CI runs them on every push. They are TypeScript, run via node's type
stripping (node 23.6+, or `--experimental-strip-types` on 22.6+), with
editor types from the `typescript_declarations` build output.