Snodo.MRTR.State (snodo v0.4.1)

Copy Markdown View Source

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.

Summary

Functions

Opens valid, unexpired state for this principal and request.

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

Functions

open(token, context, opts)

@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(data, context, opts)

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

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