Packages
atproto_browser
0.1.0
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.** Not yet published to Hex. 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
Monorepo path dependency (javascript target only):
```toml
[dependencies]
atproto_browser = { path = "../atproto_browser" }
```
## Usage
```gleam
import atproto/browser/oauth/flow
import atproto/browser/xrpc
// resolve handle -> discover auth server -> PKCE -> DPoP key -> PAR;
// returns the authorization URL to redirect the user to, plus the Flow
// state to stash for the callback.
flow.start(
xrpc.fetch_client(),
resolver: "https://slingshot.microcosm.blue",
identifier: handle,
client_id: client_id,
redirect_uri: redirect_uri,
scope: "atproto transition:generic",
extra_form: [],
)
```
After the redirect back, exchange the code with `oauth/tokens` and wrap a
client with `oauth/resource` so every request is DPoP-signed.
## 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` |
| `atproto/browser/oauth/flow` | `start()`: resolve -> discover -> PKCE -> DPoP key -> PAR |
| `atproto/browser/oauth/tokens` | `exchange_code`/`refresh`/`revoke` |
| `atproto/browser/oauth/resource` | Wraps a `Client` so every request is DPoP-signed against a token |
The rest (`pkce`, `digest`, `oauth/runner`) is internal plumbing behind
these six: PKCE/digest primitives and the effect-kernel runner that `flow`,
`tokens`, and `resource` interpret against.
## 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 # PAR + token exchange + authed call
```
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.