Rho.Auth (rho v0.1.0)

Copy Markdown View Source

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

Summary

Functions

Returns a specification to start this module under a supervisor.

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

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

What a store error means for the user.

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.

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

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

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

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

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.

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

Does provider have stored credentials (get/1)?

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

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

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

Types

credential()

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

creds()

@type creds() :: %{required(String.t()) => term()}

provider()

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

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

credential(provider)

@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(provider)

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

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

describe_error(reason)

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

What a store error means for the user.

get(provider)

@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(provider)

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

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

label(atom)

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

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

provider_for_model(arg1)

@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()

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

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

put(provider, creds)

@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(provider, key)

@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?(provider)

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

Does provider have stored credentials (get/1)?

start_link(opts \\ [])

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

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

status()

@spec status() :: %{
  required(provider()) => %{
    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()

@spec subscribe() :: :ok

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