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

The model-provider credentials, one per provider, each pi's `auth.json`
entry as JSON in the credential store (`Rho.CredentialStore`) under
`"auth:<provider>"`:

  * a subscription sign-in (omp's `/login anthropic` and
    `/login openai-codex`, `Rho.Auth.Login`): `%{"type" => "oauth",
    "access", "refresh", "expires" (ms since the epoch), "accountId",
    "email"}`, for `:anthropic` (Claude Pro/Max) and `:openai_codex`
    (ChatGPT Plus/Pro)
  * an API key (`put_api_key/2`): `%{"type" => "api_key", "key"}`, for
    `:anthropic`, `:openai` and `:google` (Gemini)

A provider holds one or the other: storing one replaces the other.

`credential/1` hands out what a model call needs: the key, or the access
token, refreshing it first when it is within 5 minutes of expiry (or
empty: a login moved over from another device may carry only the
refresh token). Refreshes run in this process, one per provider
at a time: concurrent callers wait for the same refresh. A refresh spends
the old refresh token, so its result is saved before anyone gets the new
access token; if saving fails, the new credentials stay in this process
(retried every few seconds and on every call, never refreshed again from
the spent token) and callers get `{:error, {:store_failed, reason}}` until
they are saved. A `put/2` or `delete/1` takes effect only once written;
then it wins over a refresh in flight. Subscribers (`subscribe/0`) get
`{:rho_auth, :changed}` on every `put/2`, `delete/1` and failed
refresh or save.

Reads (`get/1`, `status/0`, a key or a fresh token from `credential/1`)
don't go through the process. Secrets are never logged. Options (tests):
`:respond` answers the token endpoint instead of the network
(`Rho.Auth.OAuthFlow`), `:save_retry_ms` (5 s).

# `credential`

```elixir
@type credential() ::
  %{type: :oauth, token: String.t(), account_id: String.t() | nil}
  | %{type: :api_key, key: String.t()}
```

# `creds`

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

# `provider`

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

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `credential`

```elixir
@spec credential(provider()) ::
  {:ok, credential()}
  | {:error,
     :signed_out | {:refresh_failed, String.t()} | {:store_failed, term()}}
```

What a model call needs: `%{type: :api_key, key}`, or for a sign-in a
usable access token (and the ChatGPT account id for OpenAI Codex),
refreshed first when it's within 5 minutes of expiry.
`{:error, :signed_out}` when there is no credential;
`{:error, {:refresh_failed, message}}` when the
refresh didn't work (a dead grant needs signing in again);
`{:error, {:store_failed, reason}}` while a refreshed sign-in couldn't be
saved yet (it is retried; the old refresh token is spent, so it is never
used again).

# `delete`

```elixir
@spec delete(provider()) :: :ok | {:error, term()}
```

Signs out of `provider`; an error means the sign-in is still stored.

# `describe_error`

```elixir
@spec describe_error(term()) :: String.t()
```

What a store error means for the user.

# `get`

```elixir
@spec get(provider()) :: {:ok, creds()} | :error
```

`provider`'s stored credentials as they are (no refresh: use
`credential/1` for a model call); `:error` when there are none or the
store can't be read.

# `kinds`

```elixir
@spec kinds(provider()) :: [:oauth | :api_key]
```

How `provider` can be signed in to: `:oauth` (a subscription) and/or `:api_key`.

# `label`

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

The provider's name for the user, e.g. `"Claude (Anthropic)"`.

# `provider_for_model`

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

The provider whose credential a req_llm model spec needs:
`"anthropic:…"`, `"openai_codex:…"`, `"openai:…"`, `"google:…"`.

# `providers`

```elixir
@spec providers() :: [provider()]
```

Every provider rho can sign in to, in display order.

# `put`

```elixir
@spec put(provider(), creds()) :: :ok | {:error, term()}
```

Stores `provider`'s credentials (the shapes in the moduledoc), replacing
what it had. `{:error, :invalid_credentials}` when `creds` doesn't fit
the provider, `{:error, :unknown_provider}` for a provider rho doesn't
know, `{:error, reason}` when the store failed.

# `put_api_key`

```elixir
@spec put_api_key(provider(), String.t()) :: :ok | {:error, term()}
```

Stores an API key for `provider` (`:anthropic`, `:openai` or `:google`),
replacing its sign-in if it had one. Surrounding whitespace is dropped.

# `signed_in?`

```elixir
@spec signed_in?(provider()) :: boolean()
```

Does `provider` have stored credentials (`get/1`)?

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Started by `Rho.Supervisor` (its `:auth` option holds `opts`, see the moduledoc).

# `status`

```elixir
@spec status() :: %{
  required(provider()) =&gt; %{
    signed_in: boolean(),
    type: :oauth | :api_key | nil,
    email: String.t() | nil,
    expires: integer() | nil
  }
}
```

Per provider: whether it has a credential, which kind, and for a
sign-in its email and expiry.

# `subscribe`

```elixir
@spec subscribe() :: :ok
```

Sends the caller `{:rho_auth, :changed}` after every change, until it exits.

---

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