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

Where the OAuth client keeps tokens and registered clients.

Configure one as `store: {module, opts}` in the client's OAuth options;
`opts` is passed as the last argument of every callback. The default is
`ConduitMcp.Client.OAuth.TokenStore.ETS`: in memory, lost when the client
(or, for a `ConduitMcp.Client.Hub` server, the hub) stops. Implement this
behaviour to persist tokens (a database, the OS keychain, an encrypted
file).

## Keys

  * Tokens are stored under `{principal, issuer, resource}`: the host's
    identity for the user (`:principal`, `nil` when unused), the
    authorization server's `issuer`, and the MCP server's canonical
    resource URI. A token is therefore never used for another server,
    another authorization server, or another user - even when two
    servers trust the same authorization server (OA-5).
  * Registered clients (dynamic client registration) are stored under the
    issuer alone: credentials issued by one authorization server are
    never presented to another (OA-6).

## Values

Tokens are `ConduitMcp.Client.OAuth.Tokens` structs; the access and
refresh tokens inside are `ConduitMcp.Client.Secret`s. A client is a
string-keyed map: `"client_id"`, `"client_secret"` (a `Secret` or `nil`),
`"token_endpoint_auth_method"` and `"redirect_uris"`. A store that
serializes values must call `ConduitMcp.Client.Secret.expose/1` and wrap
again on the way back, and should encrypt them at rest.

## Single-flight refresh

`transaction/3` runs `fun` while holding a lock on `key`, so that of many
processes finding the same expired token only one refreshes it and the
others re-read the store inside the lock and find the new token. It is
optional: the default (`transaction/4` here) is a node-local
`:global.trans/4` on `{ConduitMcp.Client.OAuth.TokenStore, key}`. A store shared
across nodes (a database) should implement it with its own lock, for
example `SELECT ... FOR UPDATE`.

# `client`

```elixir
@type client() :: %{optional(String.t()) =&gt; term()}
```

# `issuer`

```elixir
@type issuer() :: String.t()
```

# `key`

```elixir
@type key() :: {principal :: term(), issuer :: String.t(), resource :: String.t()}
```

# `store`

```elixir
@type store() :: {module(), keyword()}
```

# `delete`

```elixir
@callback delete(key() | issuer(), :tokens | :client, opts :: keyword()) :: :ok
```

# `fetch_client`

```elixir
@callback fetch_client(issuer(), opts :: keyword()) :: {:ok, client()} | :error
```

# `fetch_tokens`

```elixir
@callback fetch_tokens(key(), opts :: keyword()) ::
  {:ok, ConduitMcp.Client.OAuth.Tokens.t()} | :error
```

# `put_client`

```elixir
@callback put_client(issuer(), client(), opts :: keyword()) :: :ok | {:error, term()}
```

# `put_tokens`

```elixir
@callback put_tokens(key(), ConduitMcp.Client.OAuth.Tokens.t(), opts :: keyword()) ::
  :ok | {:error, term()}
```

# `transaction`
*optional* 

```elixir
@callback transaction(key(), (-&gt; result), opts :: keyword()) :: result when result: var
```

# `transaction`

```elixir
@spec transaction(store(), key(), (-&gt; result)) :: result when result: var
```

Runs `fun` under the store's lock for `key`: the store's own
`c:transaction/3` when it has one, else a node-local
`:global.trans/4`.

---

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