# 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
```sh
gleam add voauth
```
## Quickstart
```gleam
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:
```gleam
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.
```gleam
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_refresh` | `None` | Persistence hook. |
| `call_timeout_ms` | `30_000` | Caller-side timeout for `get_token` / `refresh_now` / `set_token`. Must exceed worst-case `refresh` HTTP latency. |
| `init_timeout_ms` | `1_000` | Actor initialisation timeout. |
| `refresh_at_percent` | `80` | Proactive refresh fires at this percent of `expires_in`. |
| `min_refresh_delay_ms` | `60_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
- `RefreshFailed(RefreshRetryable(_))` — transient; voauth retries
with backoff.
- `RefreshFailed(RefreshUnauthorized(_))` — refresh token rejected;
user must reauthorise.
- `NoRefreshToken` — no token installed yet, or the installed token
has no `refresh_token`.
- `StartError(_)` — vault failed to start.
## Development
```sh
gleam test
```