# `Snodo.MRTR.State`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.4.1/lib/snodo/mrtr/state.ex#L1)

Optional, integrity-protected state for multi-round-trip requests.

`seal/3` produces a JSON-backed token that `open/3` binds to the original
request method, salient request parameters, and an explicitly supplied
authenticated principal. Supply `principal: nil` only for deliberately
anonymous requests. Authentication and principal selection belong to the
application; the helper does not infer identity from client-controlled data.

Parameters named `"_meta"`, `"requestState"`, and `"inputResponses"` at the
top level are excluded from the binding, as are the JSON-RPC request ID and
transport. Map ordering is immaterial; list ordering and numeric types are
retained, so `1` and `1.0` bind differently as a conservative check.

## Options

  * `:keys` - a keyring of `{key_id, secret}` pairs, current key first.
    `seal/3` signs with the current key and embeds its identifier in the
    token; `open/3` verifies with the key the token names, which may be the
    current key or a retired one. At most 8 keys. Identifiers are 1 to 32
    characters from `A-Z`, `a-z`, `0-9`, `_`, and `-`, and must be unique.
    Identifiers are not secret and are readable in the token.
  * `:secret` - a single key without an identifier. Tokens it seals carry no
    key identifier. Supply exactly one of `:keys` and `:secret`.
  * `:principal` - required, a JSON value.
  * `:ttl` - defaults to 300 seconds and cannot exceed 900 seconds. When
    opening a token, `:ttl` also limits its originally issued lifetime;
    changing it never extends the signed expiration.
  * `:clock` - a trusted zero-arity function returning Unix time in seconds,
    primarily for deterministic tests.

Every secret is application-managed, cryptographically random, and at least
32 bytes. Invalid options raise an `ArgumentError`.

## Rotation

A token names the key that signed it, so a key can be rotated without
invalidating tokens in flight, and nodes behind a load balancer do not have
to switch at the same moment. To replace key `a` with key `b` in a cluster:

  1. Deploy `keys: [{"a", a}, {"b", b}]` to every node. Nodes still seal
     with `a` and now also accept `b`.
  2. Once every node accepts `b`, deploy `keys: [{"b", b}, {"a", a}]`. Nodes
     seal with `b` and still accept tokens sealed with `a`.
  3. Once every node seals with `b`, wait for the largest TTL in use (at most
     900 seconds), then deploy `keys: [{"b", b}]`.

A keyring also opens tokens sealed with `:secret` when that secret is in the
keyring, and `:secret` opens tokens sealed by a keyring whose key matches
it, so a cluster can move from `:secret` to `keys: [{"a", secret}]` one node
at a time. Removing a key rejects every token it signed.

Tokens are signed, **not encrypted**: their state is readable by the client.
Never place credentials or other secrets in state. Tokens can be reused
until expiry and are not a replay-prevention or single-use mechanism. The
application owns idempotency, durable replay tracking, authorization on each
request, and secret distribution. This helper imposes a 16 KiB token size
limit.

# `open`

```elixir
@spec open(term(), Snodo.Context.t(), keyword()) ::
  {:ok, term()} | {:error, Snodo.Error.t()}
```

Opens valid, unexpired state for this principal and request.

The token is verified with the key it names, from `:keys`, or with
`:secret`. Untrusted token failures, including an unknown key identifier,
all return the same protocol error with no data or cause. Invalid
application configuration still raises an `ArgumentError`.

# `seal`

```elixir
@spec seal(term(), Snodo.Context.t(), keyword()) :: String.t()
```

Seals JSON state, raising for invalid application configuration or state.

---

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