# 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.
