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/3signs with the current key and embeds its identifier in the token;open/3verifies 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 fromA-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:keysand:secret.:principal- required, a JSON value.:ttl- defaults to 300 seconds and cannot exceed 900 seconds. When opening a token,:ttlalso 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:
- Deploy
keys: [{"a", a}, {"b", b}]to every node. Nodes still seal withaand now also acceptb. - Once every node accepts
b, deploykeys: [{"b", b}, {"a", a}]. Nodes seal withband still accept tokens sealed witha. - Once every node seals with
b, wait for the largest TTL in use (at most 900 seconds), then deploykeys: [{"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.
Summary
Functions
Opens valid, unexpired state for this principal and request.
Seals JSON state, raising for invalid application configuration or state.
Functions
@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.
@spec seal(term(), Snodo.Context.t(), keyword()) :: String.t()
Seals JSON state, raising for invalid application configuration or state.