# `Pixir.Auth.CodexOAuth`
[🔗](https://github.com/Ranvier-Technologies/pixir/blob/main/lib/pixir/auth/codex_oauth.ex#L1)

The "Sign in with ChatGPT (Codex)" OAuth flow (ADR 0002), implemented natively over
Finch. Supports **browser PKCE** (localhost callback on `127.0.0.1:1455`) and the
**device-code** fallback for headless environments. Constants and request shapes mirror
the Pi reference implementation.

Browser flow:

  1. `generate_pkce/0` + `generate_state/0` + `build_authorize_url/2`
  2. `start_callback_server/1` + `wait_for_callback/2` — capture the authorization
     code at `http://localhost:1455/auth/callback`
  3. `exchange_for_credential/3` — exchange the code at `/oauth/token`

Device-code flow:

  1. `start_device_auth/0` — POST the user-code endpoint → `{device_auth_id,
     user_code, interval}`. The user enters `user_code` at `verification_uri`.
  2. `poll_for_authorization/2` — poll the device-token endpoint until the user
     approves; the server returns an `authorization_code` *and* the PKCE
     `code_verifier` it generated.
  3. `exchange_for_credential/3` — exchange that code at `/oauth/token`

`refresh/1` swaps a refresh token for a fresh credential. All functions return
structured errors (ADR 0005).

# `credential`

```elixir
@type credential() :: %{
  kind: :subscription,
  access_token: String.t(),
  refresh_token: String.t(),
  expires_at: integer(),
  account_id: String.t(),
  obtained_at: String.t()
}
```

# `device`

```elixir
@type device() :: %{
  device_auth_id: String.t(),
  user_code: String.t(),
  interval: pos_integer(),
  verification_uri: String.t(),
  expires_in: pos_integer()
}
```

# `account_id_from_token`

```elixir
@spec account_id_from_token(String.t()) ::
  {:ok, String.t()} | {:error, :no_account_id}
```

Extract `chatgpt_account_id` from an access token's JWT claims.

# `browser_redirect_uri`

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

Registered redirect URI for the browser OAuth flow.

# `build_authorize_url`

```elixir
@spec build_authorize_url(map(), String.t(), keyword()) :: String.t()
```

Build the browser authorize URL for the user to open.

# `close_callback_server`

```elixir
@spec close_callback_server(port()) :: :ok
```

Close the callback listen socket.

# `exchange_for_credential`

```elixir
@spec exchange_for_credential(String.t(), String.t(), keyword()) ::
  {:ok, credential()} | {:error, map()}
```

Exchange an authorization code (+ verifier) for a stored-shape credential.

Pass `redirect_uri:` for the browser flow; device-code uses the device callback URI by
default.

# `generate_pkce`

```elixir
@spec generate_pkce() :: %{code_verifier: String.t(), code_challenge: String.t()}
```

Generate PKCE verifier/challenge pair for the browser flow.

# `generate_state`

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

Generate an OAuth `state` value for CSRF protection.

# `poll_for_authorization`

```elixir
@spec poll_for_authorization(
  device(),
  keyword()
) ::
  {:ok, %{authorization_code: String.t(), code_verifier: String.t()}}
  | {:error, map()}
```

Poll until the user approves (or the flow times out). Honors `slow_down` by widening
the interval (RFC 8628). Returns `{authorization_code, code_verifier}`.

# `refresh`

```elixir
@spec refresh(String.t()) :: {:ok, credential()} | {:error, map()}
```

Refresh an expired/expiring subscription credential.

# `refresh_skew_ms`

How long before expiry we proactively refresh (ms).

# `start_callback_server`

```elixir
@spec start_callback_server(keyword()) :: {:ok, port()} | {:error, map()}
```

Start the localhost callback listener. Accepts `:host`, `:port` (testing).

# `start_device_auth`

```elixir
@spec start_device_auth() :: {:ok, device()} | {:error, map()}
```

Begin device authorization. Returns device info to display to the user.

# `wait_for_callback`

```elixir
@spec wait_for_callback(
  port(),
  keyword()
) :: {:ok, String.t()} | {:error, map()}
```

Wait for one browser callback. Requires `:state`; accepts `:timeout_ms`.

---

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