# `AttestoMCP.Server.SessionStore`
[🔗](https://github.com/XukuLLC/attesto_mcp_server/blob/v2.8.0/lib/attesto_mcp_server/session_store.ex#L1)

Durable session storage contract for the session-bound MCP revisions.

Keys are `{namespace, session_id}` data tuples. Adapters must never convert
either component to an atom. `update/3`, `update_ttl/3`, and
`cleanup_expired/1` must be atomic with respect to one another. Records are
the versioned JSON-compatible maps documented by
`AttestoMCP.Server.Session.to_record/1`; unknown fields must be preserved by
an adapter and ignored by the server. Records with a future
`format_version` are opaque to an older node and must remain untouched by
ordinary loads, updates, touches, and expiry cleanup. A preserved future
record returned by `update/3` or `update_ttl/3` is reported as
`{:ok, opaque_record}` so callers can distinguish it from an absent row.
`update_ttl/3` must delete an already expired record with a version understood
by the adapter and return `:not_found` rather than renewing it. On success it
must return the updated record. If the supplied timestamp is older than the
record's current `"last_seen_ms"`, adapters must retain the newer value; a
successful touch must never rewind session activity. Adapters may implement
`count_active/1` or namespace-aware `count_active/2` to let server stats
count sessions without materializing bounded record pages; otherwise the
server falls back to `list_active/1`. Adapters should implement
`count_active_for_principal/2` or its namespace-aware arity-three form so the
server can enforce its per-principal session limit without materializing all
active records. Shared adapters should also
implement `save_below_principal_limit/4` as one atomic count-and-save
operation so concurrent server processes cannot exceed that limit.
They may also implement `list_active_keys/4` to provide bounded, cursor-based
operator visibility without loading principal bindings or complete session
records. The returned keys must be scoped to the requested namespace, ordered
by session ID in ascending binary/UTF-8 order, and accompanied by the last
returned ID as `next_cursor` only when more keys remain. For externally
supplied keys, `load/2`, `update/3`, and `update_ttl/3` should treat malformed or
unrepresentable keys as absent, while `delete/2` should remain idempotent;
valid keys from another configured namespace must still return the
namespace-mismatch error. `cleanup_expired/1` must return no more than 1,000
valid keys per call; adapters with more expired records must clean them in
bounded batches across later calls.

# `active_key_page`

```elixir
@type active_key_page() :: %{keys: [key()], next_cursor: String.t() | nil}
```

# `adapter`

```elixir
@type adapter() :: {module(), store()}
```

A configured adapter module and its opaque store handle.

# `key`

```elixir
@type key() :: {String.t(), String.t()}
```

# `session_record`

```elixir
@type session_record() :: map()
```

# `store`

```elixir
@type store() :: term()
```

# `update_fun`

```elixir
@type update_fun() :: (session_record() -&gt;
                   {:ok, session_record()} | :delete | {:error, term()})
```

# `cleanup_expired`

```elixir
@callback cleanup_expired(store()) :: {:ok, [key()]} | {:error, term()}
```

# `count_active`
*optional* 

```elixir
@callback count_active(store()) :: {:ok, non_neg_integer()} | {:error, term()}
```

# `count_active`
*optional* 

```elixir
@callback count_active(store(), String.t()) :: {:ok, non_neg_integer()} | {:error, term()}
```

# `count_active_for_principal`
*optional* 

```elixir
@callback count_active_for_principal(store(), term()) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

# `count_active_for_principal`
*optional* 

```elixir
@callback count_active_for_principal(store(), String.t(), term()) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

# `delete`

```elixir
@callback delete(store(), key()) :: :ok | {:error, term()}
```

# `list_active`

```elixir
@callback list_active(store()) :: {:ok, [{key(), session_record()}]} | {:error, term()}
```

# `list_active_keys`
*optional* 

```elixir
@callback list_active_keys(store(), String.t(), String.t() | nil, pos_integer()) ::
  {:ok, active_key_page()} | {:error, term()}
```

# `load`

```elixir
@callback load(store(), key()) :: {:ok, session_record()} | :not_found | {:error, term()}
```

# `save`

```elixir
@callback save(store(), key(), session_record()) :: :ok | {:error, term()}
```

# `save_below_principal_limit`
*optional* 

```elixir
@callback save_below_principal_limit(store(), key(), session_record(), pos_integer()) ::
  :ok | :limit_reached | {:error, term()}
```

# `update`

```elixir
@callback update(store(), key(), update_fun()) ::
  {:ok, session_record()} | :not_found | {:error, term()}
```

# `update_ttl`

```elixir
@callback update_ttl(store(), key(), non_neg_integer()) ::
  {:ok, session_record()} | :not_found | {:error, term()}
```

---

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