voauth

OAuth2 access-token vault for Gleam. Proactive refresh, bounded retry, typed errors.

What's voauth?

When you call an OAuth2 API you have to renew the access token before it expires. Done well, that needs a long-lived process holding the current token, a timer to refresh before expiry, retry on transient failures, and a way to tell the UI "the user has to reconnect" when the refresh token gets revoked.

voauth is that process. You write the bit that talks to your provider's /oauth2/token endpoint; voauth handles the rest.

Installation

gleam add voauth

Quickstart

import gleam/option.{Some}
import voauth

pub fn main() {
  // 1. Build a config with sane defaults; override what you need.
  let config =
    voauth.config(refresh: my_refresh_function)
    |> voauth.with_on_refresh(my_persist_callback)

  // 2. Start the vault.
  let assert Ok(vault) = voauth.start(config)

  // 3. After OAuth (or rehydrating from your DB), install a token.
  voauth.set_token(vault, voauth.Token(
    access_token: "...",
    expires_in: 1800,
    refresh_token: Some("..."),
    scope: "...",
    token_type: "Bearer",
  ))

  // 4. Use the vault. Blocks until a valid access token is available
  //    (refreshes automatically if the cached one has expired).
  case voauth.get_token(vault) {
    Ok(token) -> // call your provider's API with `token`
    Error(voauth.RefreshFailed(voauth.RefreshUnauthorized(_))) ->
      // refresh token revoked — show "please reconnect" UI
    Error(voauth.RefreshFailed(voauth.RefreshRetryable(_))) ->
      // transient outage — caller may retry
    Error(voauth.NoRefreshToken) ->
      // no token installed yet — user hasn't authorised
    Error(voauth.StartError(_)) -> // vault start error
  }
}

The refresh callback

Provider-specific. Performs the OAuth2 refresh-token grant and returns either a RefreshResponse or a typed error:

fn my_refresh_function(
  refresh_token: String,
) -> Result(voauth.RefreshResponse, voauth.RefreshError) {
  // ... HTTP call to your provider's /oauth2/token endpoint ...
  case status, body {
    200, body -> decode_refresh_response(body)

    // OAuth2 invalid_grant / invalid_token: refresh token is dead.
    400, body | 401, body if has_invalid_grant(body) ->
      Error(voauth.RefreshUnauthorized(body))

    // Everything else: treat as transient; voauth will retry with backoff.
    _, body -> Error(voauth.RefreshRetryable(body))
  }
}

Use voauth.refresh_response_decoder() for the JSON. voauth handles the case where a provider omits fields like scope or refresh_token on a refresh — merge_response carries the previous values forward.

The on_refresh callback (optional)

Fired after every successful refresh. Use it to persist the new token to durable storage (DB, file, secrets manager) so a process restart can rehydrate from the latest state.

fn my_persist_callback(token: voauth.Token) -> Result(Nil, String) {
  save_to_db(token)
}

The callback runs inside the vault's mailbox, so keep it fast. Errors are logged at Error level via logging and otherwise ignored; the vault keeps running.

Configuration

config(refresh:) returns a Config with the defaults below. Override with the with_* setters.

Field Default Description
on_refreshNone Persistence hook.
call_timeout_ms30_000 Caller-side timeout for get_token / refresh_now / set_token. Must exceed worst-case refresh HTTP latency.
init_timeout_ms1_000 Actor initialisation timeout.
refresh_at_percent80 Proactive refresh fires at this percent of expires_in.
min_refresh_delay_ms60_000 Floor on the proactive delay.
retry_backoff_ms[30_000, 60_000, 120_000] Backoff schedule for failed scheduled refreshes. [] disables retries.

Crash recovery

The vault actor can be supervised by your application's gleam_otp supervisor. voauth.supervised(config) returns a child specification. On restart the supervisor calls start(config) with the same config.

To survive a full BEAM restart, persist tokens via on_refresh and re-install via set_token from your durable store on application startup. voauth doesn't own a persistence backend.

Errors

Development

gleam test