# `Ltix.OAuth.Client`
[🔗](https://github.com/DecoyLex/ltix/blob/main/lib/ltix/oauth/client.ex#L1)

Authenticated OAuth session for LTI Advantage service calls.

Holds an access token, tracks which scopes were granted, and provides
explicit refresh. Pass this struct to service functions like
`Ltix.MembershipsService.get_members/2`.

## Checking expiry

    client =
      if Ltix.OAuth.Client.expired?(client) do
        Ltix.OAuth.Client.refresh!(client)
      else
        client
      end

## Reusing tokens across contexts

A token is valid across contexts (courses, launches) on the same
registration. Reuse a cached token with different endpoints:

    {:ok, client_b} = Ltix.OAuth.Client.with_endpoints(client, %{
      Ltix.MembershipsService => course_b_endpoint
    })

Or build from a previously cached `AccessToken`:

    {:ok, client} = Ltix.OAuth.Client.from_access_token(cached_token,
      registration: registration,
      endpoints: %{Ltix.MembershipsService => endpoint}
    )

# `t`

```elixir
@type t() :: %Ltix.OAuth.Client{
  access_token: String.t(),
  endpoints: %{required(module()) =&gt; term()},
  expires_at: DateTime.t(),
  registration: Ltix.Registration.t(),
  req_options: keyword(),
  scopes: MapSet.t(String.t())
}
```

# `expired?`

```elixir
@spec expired?(t()) :: boolean()
```

Check whether the client's token has expired.

Uses a 60-second buffer to avoid using a token that is about to expire.

## Examples

    iex> client = %Ltix.OAuth.Client{
    ...>   access_token: "tok",
    ...>   expires_at: DateTime.add(DateTime.utc_now(), 3600),
    ...>   scopes: MapSet.new(),
    ...>   registration: nil,
    ...>   req_options: []
    ...> }
    iex> Ltix.OAuth.Client.expired?(client)
    false

# `from_access_token`

```elixir
@spec from_access_token(
  Ltix.OAuth.AccessToken.t(),
  keyword()
) :: {:ok, t()} | {:error, Exception.t()}
```

Build a client from a cached `AccessToken`.

Validates endpoints and checks that the token's granted scopes cover
the required scopes for all endpoints.

## Options

  * `:registration` (required) - the `Ltix.Registration` for refresh
  * `:endpoints` (required) - map of service modules to endpoint structs
  * `:req_options` - options passed through to `Req.request/2` (default: `[]`)

# `from_access_token!`

```elixir
@spec from_access_token!(
  Ltix.OAuth.AccessToken.t(),
  keyword()
) :: t()
```

Same as `from_access_token/2` but raises on error.

# `has_scope?`

```elixir
@spec has_scope?(t(), String.t()) :: boolean()
```

Check whether the client was granted a specific scope.

## Examples

    iex> client = %Ltix.OAuth.Client{
    ...>   access_token: "tok",
    ...>   expires_at: DateTime.utc_now(),
    ...>   scopes: MapSet.new(["scope:read"]),
    ...>   registration: nil,
    ...>   req_options: []
    ...> }
    iex> Ltix.OAuth.Client.has_scope?(client, "scope:read")
    true
    iex> Ltix.OAuth.Client.has_scope?(client, "scope:write")
    false

# `refresh`

```elixir
@spec refresh(t()) :: {:ok, t()} | {:error, Exception.t()}
```

Re-acquire the token using the stored registration and endpoints.

Re-derives requested scopes from endpoints via each service's `scopes/1`
callback, so a transient partial grant does not become permanent.

# `refresh!`

```elixir
@spec refresh!(t()) :: t()
```

Same as `refresh/1` but raises on error.

# `require_any_scope`

```elixir
@spec require_any_scope(t(), [String.t()]) :: :ok | {:error, Exception.t()}
```

Require any one of the given scopes.

Returns `:ok` if at least one scope from the list was granted.

# `require_scope`

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

Require a specific scope, returning an error if not granted.

# `with_endpoints`

```elixir
@spec with_endpoints(t(), %{required(module()) =&gt; term()}) ::
  {:ok, t()} | {:error, Exception.t()}
```

Swap endpoints on an existing client.

Validates the new endpoints and checks that the client's granted scopes
cover the required scopes. The token remains the same.

# `with_endpoints!`

```elixir
@spec with_endpoints!(t(), %{required(module()) =&gt; term()}) :: t()
```

Same as `with_endpoints/2` but raises on error.

---

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