# `Attesto.PresentationSession`
[🔗](https://github.com/XukuLLC/attesto/blob/v2.0.1/lib/attesto/presentation_session.ex#L1)

Verifier-side OID4VP presentation-session state machine.

`create/3` persists the nonce, audience, requested DCQL IDs, and issuer trust
needed to verify a later `direct_post` response. The opaque session `id` is
also the OID4VP `state` value, so a wallet response can be correlated without
a second index or identifier. An optional `:response_uri` attr is also
stored and forwarded to `Attesto.VpToken.verify/2`; it is required only if
the session expects an `mso_mdoc` presentation (see `Attesto.VpToken`'s
moduledoc) and may be omitted for SD-JWT-VC-only sessions exactly as before.

Verification delegates all SD-JWT VC / mdoc and holder-binding cryptography
to `Attesto.VpToken.verify/2`. A malformed or invalid presentation leaves the
session pending, allowing a valid response to arrive before expiry. Only a
successfully verified response attempts the store's atomic completion.

A `{:resolve_issuer, fun}` trust source contains an in-memory function and is
therefore suitable only for in-memory stores such as the bundled ETS store.
Persistent stores must use static `{:issuer_jwks, jwks}` trust material (or
define their own serializable trust-reference convention outside this core
primitive).

Store callbacks are checked against `Attesto.PresentationSessionStore`'s
documented return contracts. An unexpected return or malformed stored entry
raises a constant `RuntimeError`; callback values are never copied into the
exception message. This is especially important for `result/2`, because
`take/1` may already have consumed the only copy before its return is
validated.

# `correlation`

```elixir
@type correlation() :: {:state, String.t()} | {:id, String.t()}
```

# `create_attrs`

```elixir
@type create_attrs() :: %{
  :audience =&gt; String.t(),
  :expected_query_ids =&gt; [String.t()],
  :issuer_trust =&gt; issuer_trust(),
  optional(:request_object) =&gt; String.t(),
  optional(:response_uri) =&gt; String.t(),
  optional(:query_constraints) =&gt; map()
}
```

# `issuer_trust`

```elixir
@type issuer_trust() ::
  {:issuer_jwks, map() | list()} | {:resolve_issuer, (String.t() -&gt; term())}
```

# `attach_request_object`

```elixir
@spec attach_request_object(module(), String.t(), String.t()) ::
  :ok | {:error, :unavailable}
```

Attach the signed OID4VP request object to a pending session.

Called once at creation time (the request object needs the session's `nonce`
and its `id`/`state`, which `create/3` generates). Atomic on the pending
status. Returns `{:error, :unavailable}` if the session is unknown, expired,
or already completed.

# `attach_response_encryption_jwk`

```elixir
@spec attach_response_encryption_jwk(module(), String.t(), map()) ::
  :ok | {:error, :unavailable}
```

Attach the verifier's per-request response-encryption private JWK to a pending
session (its `kid` is the session id), so the direct-post endpoint can decrypt
a `direct_post.jwt` response with the ephemeral key it advertised.

# `create`

```elixir
@spec create(module(), create_attrs(), keyword()) ::
  {:ok, %{id: String.t(), nonce: String.t()}} | {:error, :invalid_attrs}
```

Create and persist a short-lived OID4VP presentation session.

The returned `id` is both the store key and the request's `state`; `nonce`
belongs in the presentation request. Options are `:ttl` (default
`300` seconds) and `:now` (a clock override).

# `request_object`

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

Read a pending session's stored request object (the signed OID4VP request
object the interface serves at its `request_uri`), if one was persisted at
`create/3` via the optional `:request_object` attr. Returns `:error` for an
unknown, expired, or request-object-less session.

# `response_encryption_jwk`

```elixir
@spec response_encryption_jwk(module(), String.t()) :: {:ok, map()} | :error
```

Read a pending session's per-request response-encryption private JWK, if one
was attached. Returns `:error` for an unknown, expired, or key-less session.

# `result`

```elixir
@spec result(module(), String.t()) :: {:ok, map()} | :error
```

Read and consume a completed session's verified result. Single-use.

The result is returned at most once: the completed session is atomically
removed on read (via the store's `take/1`). This bounds exposure of the
presented — potentially PII — claims. The `response_code` a verifier hands the
browser to trigger this read is the session id, and it transits the browser
address bar, history, `Referer`, and logs; a non-consuming read would let
anyone who later captured that value replay it to re-read the claims for the
rest of the session TTL. Single-use closes that: the verifier front-end reads
the result once, on the completion redirect, and a captured `response_code` is
dead afterwards. A second read (or a read of a still-pending/expired session)
returns `:error`.

Returns the same shape as `verify_response/4` — the VpToken results map
directly — so the live-return and read-back paths handle one shape, not two.

# `verify_response`

```elixir
@spec verify_response(module(), correlation(), map(), keyword()) ::
  {:ok, map()}
  | {:error,
     :unknown_session
     | :expired
     | :already_completed
     | {:invalid_presentation, term()}}
```

Verify a wallet response and atomically complete its pending session.

Invalid presentations return `{:invalid_presentation, reason}` and do not
complete the session. When concurrent valid responses race, exactly one can
complete it; all losing calls return `:already_completed`.

---

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