# `Rho.Auth.OAuthFlow`
[🔗](https://github.com/GenericJam/rho/blob/v0.1.0/lib/rho/auth/oauth_flow.ex#L1)

The OAuth authorization-code flows omp signs in with (`/login anthropic`,
`/login openai-codex`), built as omp's declarative engine builds them from
`pi-catalog`'s `rules/auth/anthropic.kdl` and `openai-codex.kdl`:

  * **Anthropic** (Claude Pro/Max): `https://claude.ai/oauth/authorize`
    with `code=true`, PKCE S256, redirect `http://localhost:54545/callback`;
    the token endpoint `https://api.anthropic.com/v1/oauth/token` takes a
    JSON body (the exchange also sends `state`; a refresh sends Claude
    Code's `anthropic-beta` and `User-Agent` headers). Access tokens live
    ~8 h and are stored 5 min short; each refresh rotates the refresh
    token, and the grant dies ~30 days after the interactive login.
  * **OpenAI Codex** (ChatGPT Plus/Pro): `https://auth.openai.com/oauth/authorize`,
    PKCE S256, redirect exactly `http://localhost:1455/auth/callback` (the
    only one OpenAI allows); `https://auth.openai.com/oauth/token` takes a
    form body. The ChatGPT account id comes from the access/id token's
    `https://api.openai.com/auth` claim.

Credentials are pi's `auth.json` entry: `%{"type" => "oauth", "access",
"refresh", "expires" (ms since the epoch), "accountId", "email"}` (the
last two only when known).

HTTP: `opts[:respond]` (tests) answers a request instead of the network,
`fn %Req.Request{} -> %Req.Response{} end`, seeing it fully encoded.

# `creds`

```elixir
@type creds() :: %{required(String.t()) =&gt; term()}
```

# `flow`

```elixir
@type flow() :: %{verifier: String.t(), state: String.t()}
```

# `provider`

```elixir
@type provider() :: :anthropic | :openai_codex
```

# `challenge`

```elixir
@spec challenge(String.t()) :: String.t()
```

PKCE S256: base64url(sha256(verifier)), unpadded.

# `default_redirect`

```elixir
@spec default_redirect(provider()) :: String.t()
```

The provider's registered localhost redirect URI (the one `start/2` normally gets).

# `exchange`

```elixir
@spec exchange(provider(), String.t(), flow(), String.t(), keyword()) ::
  {:ok, creds()} | {:error, String.t()}
```

Exchanges an authorization code for credentials. A `code#state` (what
Anthropic's code page shows) is split, the pasted state winning, as omp
does. `opts`: `:respond`, and `:retry_until` (a monotonic ms deadline:
transport errors are retried every 2 s until then, for a phone whose
network Android cuts while the browser is in front).

# `refresh`

```elixir
@spec refresh(provider(), creds(), keyword()) :: {:ok, creds()} | {:error, String.t()}
```

Trades `creds`' refresh token for new credentials. The refresh token
rotates when the response carries one; identity the response lacks is
kept from `creds`.

# `start`

```elixir
@spec start(provider(), String.t()) :: %{
  url: String.t(),
  verifier: String.t(),
  state: String.t()
}
```

A new PKCE verifier and state, and the authorize URL for them: the
standard parameters in omp's order, then the provider's extras.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
