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

An authorization-code flow between the authorization request and the
callback: everything the client must remember to validate the response
and redeem the code.

`new/1` generates the two secrets of the flow: a `state` of 32 random bytes
(base64url, 43 characters) and a PKCE code verifier (`ConduitMcp.Client.OAuth.PKCE.verifier/0`),
both wrapped in `ConduitMcp.Client.Secret`, and stamps a 10 minute TTL. It
records the issuer the flow was started against (for the RFC 9207 `iss`
check), the canonical resource, the redirect URI (the token request must
repeat it exactly), the requested scopes and the principal - the end-user
or session identity of the host - that the flow is bound to.

## The host stores it

`Pending` is plain data the host keeps between the redirect and the
callback: in its session store, a database row, or an encrypted cookie.
`to_map/1` gives a string-keyed map of strings, integers, booleans and
lists (plus the principal as given) for that; `from_map/1` validates and
restores it. Restore the struct before calling
`ConduitMcp.Client.OAuth.Callback.validate/3`.

**The map holds secrets in plain text** (`state` and `code_verifier`).
Keep it server-side, or encrypt it (for example with
`Plug.Crypto.encrypt/4`); signing alone does not hide it. `inspect/1` of
the struct shows both as `#Secret<redacted>`.

## Single use is the host's job

`state` must be usable exactly once (OA-2). Because `Pending` is data, the
library cannot see a second copy being presented, so a replayed callback
is rejected only if the host's store enforces it: **look the pending entry
up by `state` and delete it in the same step, before validating, and on
failure as well as on success.** `ConduitMcp.Client.OAuth.Callback.validate/3`
checks the state value, principal binding, expiry and `iss`; it does not
and cannot check consumption.

## Serialization of the principal

The principal is stored in `to_map/1` exactly as given. A host that
JSON-encodes the map should use a string principal.

# `t`

```elixir
@type t() :: %ConduitMcp.Client.OAuth.Pending{
  client_id: String.t() | nil,
  created_at: integer(),
  expires_at: integer(),
  iss_parameter_supported: boolean(),
  issuer: String.t(),
  principal: term(),
  redirect_uri: String.t(),
  resource: String.t(),
  scopes: [String.t()],
  state: ConduitMcp.Client.Secret.t(),
  token_endpoint: String.t() | nil,
  verifier: ConduitMcp.Client.Secret.t()
}
```

* `:state`, `:verifier` - the flow's secrets.
* `:redirect_uri` - sent in the authorization request; must be repeated in
  the token request.
* `:issuer` - the authorization server's `issuer`, as validated.
* `:resource` - the canonical resource URI (RFC 8707 `resource`).
* `:scopes` - the requested scopes.
* `:principal` - the host identity the flow is bound to (`nil` if none).
* `:iss_parameter_supported` - the AS metadata's
  `authorization_response_iss_parameter_supported` was `true`.
* `:client_id`, `:token_endpoint` - the client identity and token endpoint
  the flow was started with, so the code is redeemed at the same
  authorization server (`nil` when not recorded).
* `:created_at`, `:expires_at` - Unix seconds.

# `code_challenge`

```elixir
@spec code_challenge(t()) :: String.t()
```

The PKCE `S256` code challenge for the pending verifier.

# `expired?`

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

Whether the pending authorization has expired at Unix time `now`.

# `from_map`

```elixir
@spec from_map(term()) :: {:ok, t()} | {:error, ConduitMcp.Client.OAuth.Error.t()}
```

Restores a map from `to_map/1`. Every field is type-checked; a missing or
malformed one gives `:invalid_pending` with `%{field: name}` as data (the
value itself is never put in the error). The expiry is not checked here.

# `new`

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

Starts a pending authorization with a fresh `state` and code verifier.

Raises `NimbleOptions.ValidationError` on invalid options.

## Options

* `:redirect_uri` - Required. An `https` URL, or `http` on a loopback host (RFC 8252 §7.3); no fragment.

* `:issuer` (`t:String.t/0`) - Required. The validated `issuer`.

* `:resource` (`t:String.t/0`) - Required. The canonical resource URI.

* `:scopes` (list of `t:String.t/0`) - The requested scopes. The default value is `[]`.

* `:principal` (`t:term/0`) - The host identity to bind the flow to. The default value is `nil`.

* `:iss_parameter_supported` (`t:boolean/0`) - The AS advertises `authorization_response_iss_parameter_supported: true`. The default value is `false`.

* `:client_id` - The client id used. The default value is `nil`.

* `:token_endpoint` - The validated token endpoint. The default value is `nil`.

* `:ttl` (`t:pos_integer/0`) - Lifetime in seconds. The default value is `600`.

* `:now` (`t:integer/0`) - Current Unix time in seconds (default: the system clock).

# `to_map`

```elixir
@spec to_map(t()) :: %{required(String.t()) =&gt; term()}
```

The pending authorization as a string-keyed map for host storage. It
contains `state` and `code_verifier` in plain text; see the moduledoc.

---

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