# `ConduitMcp.Client.OAuth.Tokens`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/client/oauth/tokens.ex#L1)

The tokens the client holds for one `{principal, issuer, resource}`.

`access_token` and `refresh_token` are `ConduitMcp.Client.Secret`s, so
`inspect/1`, logs and crash reports show `#Secret<redacted>`; the bearer
is exposed only where the `Authorization` header is built. `expires_at` is
Unix seconds computed locally from `expires_in` (`nil` when the server gave
no lifetime).

    iex> tokens = ConduitMcp.Client.OAuth.Tokens.new(access_token: "at", expires_at: 1_000)
    iex> inspect(tokens) =~ "at\""
    false
    iex> ConduitMcp.Client.OAuth.Tokens.expired?(tokens, 960, 30)
    false
    iex> ConduitMcp.Client.OAuth.Tokens.expired?(tokens, 970, 30)
    true

# `t`

```elixir
@type t() :: %ConduitMcp.Client.OAuth.Tokens{
  access_token: ConduitMcp.Client.Secret.t(),
  expires_at: integer() | nil,
  refresh_token: ConduitMcp.Client.Secret.t() | nil,
  scopes: [String.t()],
  token_type: String.t()
}
```

# `expired?`

```elixir
@spec expired?(t(), integer(), non_neg_integer()) :: boolean()
```

Whether the access token is expired, or expires within `skew_seconds`, at
Unix time `now`: `now + skew_seconds >= expires_at`. Tokens without
`expires_at` never expire by time (a 401 still triggers a refresh).

# `new`

```elixir
@spec new(keyword()) :: t()
```

Builds tokens from `fields`, wrapping `:access_token` and `:refresh_token`
in `ConduitMcp.Client.Secret` (values already wrapped are kept).

Raises `ArgumentError` when `:access_token` is missing or a field has the
wrong type, and `KeyError` on an unknown field.

---

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