brazier/session
Types
Storage operations for an External backend. Each returns
Error(reason) when the storage fails; Brazier logs it and returns
SessionBackendFailed(reason). Every operation must be atomic on its
own; Brazier builds updates and rotation from them.
pub type ExternalBackend {
ExternalBackend(
name: String,
create: fn(StoredSession) -> Result(Bool, String),
fetch: fn(String) -> Result(
option.Option(StoredSession),
String,
),
touch: fn(String, Int) -> Result(Bool, String),
replace: fn(StoredSession, Int) -> Result(Bool, String),
delete: fn(String) -> Result(Nil, String),
delete_version: fn(String, Int) -> Result(Bool, String),
expired: fn(Int, Int, Int) -> Result(
List(#(String, Int)),
String,
),
count: fn() -> Result(Int, String),
)
}
Constructors
-
ExternalBackend( name: String, create: fn(StoredSession) -> Result(Bool, String), fetch: fn(String) -> Result( option.Option(StoredSession), String, ), touch: fn(String, Int) -> Result(Bool, String), replace: fn(StoredSession, Int) -> Result(Bool, String), delete: fn(String) -> Result(Nil, String), delete_version: fn(String, Int) -> Result(Bool, String), expired: fn(Int, Int, Int) -> Result( List(#(String, Int)), String, ), count: fn() -> Result(Int, String), )Arguments
- name
-
Names the store in logs and in the live-connection registry. Every node using the same storage must use the same name.
- create
-
Insert a new row.
Ok(False)when the id already exists. - fetch
-
The row for an id,
Ok(None)when there is none. Lifetimes are checked by Brazier. - touch
-
Set a row’s
last_seen_ms.Ok(False)when the row is gone. Must not create a row. - replace
-
Replace the row with the same id only if its version is still the given one (compare-and-swap).
Ok(False)when it is not. - delete
-
Delete a row. Deleting a missing row succeeds.
- delete_version
-
Delete a row only if its version is still the given one.
- expired
-
Up to
limit#(id, version)pairs of rows created before the first cutoff or last seen before the second. - count
-
The number of rows.
Why a bound process was told its session is gone.
pub type RevocationReason {
Deleted
Rotated
Expired
}
Constructors
-
DeletedThe session was deleted (logout), or was found missing when the bindings were re-checked after a node connected.
-
RotatedThe session id was replaced by
rotate(login or privilege change). -
ExpiredThe session’s absolute or idle lifetime passed.
Where a store keeps its sessions.
pub type SessionBackend {
LocalEts(name: String)
External(backend: ExternalBackend)
}
Constructors
-
LocalEts(name: String)An ETS table on this node. Sessions are not shared between nodes.
-
External(backend: ExternalBackend)Functions over shared storage, used the same way from every node.
A session store handle.
pub opaque type SessionStore
Why a session operation failed.
pub type SessionStoreError {
SessionNotFound
SessionExpired
SessionContention
SessionBackendFailed(reason: String)
InvalidBackend(reason: String)
}
Constructors
-
SessionNotFoundThe session does not exist: never created, deleted, or rotated away.
-
SessionExpiredThe session existed but its absolute or idle lifetime has passed.
-
SessionContentionThe compare-and-swap lost too many races in a row. This indicates a pathological write storm on one session and is reported, never hidden.
-
SessionBackendFailed(reason: String)An
Externalbackend operation failed. -
InvalidBackend(reason: String)The backend configuration is invalid.
Opaque ETS table reference.
pub type SessionTable
Lifetime configuration for a session store.
pub type StoreConfig {
StoreConfig(
absolute_ttl_ms: Int,
idle_ttl_ms: Int,
sweep_interval_ms: Int,
)
}
Constructors
-
StoreConfig( absolute_ttl_ms: Int, idle_ttl_ms: Int, sweep_interval_ms: Int, )Arguments
- absolute_ttl_ms
-
Maximum age of a session from creation, in milliseconds.
- idle_ttl_ms
-
Maximum time between accesses, in milliseconds.
- sweep_interval_ms
-
How often the background sweeper deletes expired sessions.
A session row as a backend stores it. version increases on every data
write; times are erlang:system_time(millisecond).
pub type StoredSession {
StoredSession(
session: Session,
version: Int,
created_at_ms: Int,
last_seen_ms: Int,
)
}
Constructors
-
StoredSession( session: Session, version: Int, created_at_ms: Int, last_seen_ms: Int, )
Values
pub fn bind_current_process(
store: SessionStore,
session_id: String,
) -> Nil
Bind the calling process (a WebSocket handler) to a session.
When the session is deleted, rotated, or expires (on any connected
node), the process receives {brazier_session_revoked, SessionId, ReasonBinary}. Binding is idempotent and is released automatically when
the process exits.
pub fn binding_fingerprint(session_id: String) -> String
Lowercase hex SHA-256 of a session id.
Embed this (never the raw id, which is a bearer secret) in signed tokens that must be bound to a session, such as the WebSocket join token.
pub fn bound_process_count(
store: SessionStore,
session_id: String,
) -> Int
How many live processes on this node are bound to a session id.
pub const clock_skew_tolerance_ms: Int
How far apart node clocks may be. A stored timestamp up to this far in the future is accepted; one further ahead is rejected.
pub fn count(store: SessionStore) -> Int
Number of rows currently stored (including not-yet-swept expired rows). Crashes the calling process if an External backend fails.
pub fn create(store: SessionStore) -> Session
Create a new session with a generated ID.
Crashes the calling process if an External backend fails (see
try_create_with_data).
pub fn create_with_data(
store: SessionStore,
data: dict.Dict(String, String),
) -> Session
Create a new session with a generated ID and initial data, in one write.
Crashes the calling process if an External backend fails (see
try_create_with_data).
pub const default_absolute_ttl_ms: Int
Default absolute lifetime: 24 hours.
pub fn default_store_config() -> StoreConfig
Default store lifetimes (24h absolute, 2h idle, sweep every minute).
pub const default_sweep_interval_ms: Int
Default sweep interval: 60 seconds.
pub fn delete(store: SessionStore, id: String) -> Nil
Delete a session (logout). Bound WebSockets on every node are told to
close. Crashes the calling process if an External backend fails (see
try_delete).
pub fn ensure_registry() -> Nil
Start the live-connection registry scope, not linked to the caller, when it is not running.
brazier.start owns the scope in its supervision tree; call this only to
bind sessions without it (application.start, tests, embedding). A
supervised start fails when the scope already runs unsupervised.
pub fn fetch(
store: SessionStore,
id: String,
) -> Result(Session, SessionStoreError)
Get a live session by ID and refresh its idle deadline, returning why there is none.
pub fn generate_id() -> String
Generate a cryptographically random, URL-safe token.
Used for session IDs and session-bound CSRF tokens.
pub fn get(
store: SessionStore,
id: String,
) -> option.Option(Session)
Get a live session by ID and refresh its idle deadline.
Returns None for unknown, deleted, rotated, or expired sessions. Expired
sessions are deleted on the spot and their bound sockets are notified.
Crashes the calling process if an External backend fails (see fetch).
pub fn get_value(
session: Session,
key: String,
) -> option.Option(String)
Get a value from a session.
pub fn id_from_request(
req: request.Request(body),
cookie_name: String,
) -> Result(String, Nil)
Read the raw session id from a request cookie.
Does NOT check the store; use get to validate the session.
pub fn is_current_process_bound(
store: SessionStore,
session_id: String,
) -> Bool
Whether the calling process is bound to the session (by
bind_current_process). A WebSocket re-check uses it to tell a socket
that signed in from one that never did: once bound, the session going
away must close the socket, never turn it into a guest.
pub fn new_store(name: String) -> SessionStore
Create a new LocalEts session store with default lifetimes.
The ETS table is owned by the calling process; the maintenance process (sweeper and re-checks) exits when that process exits.
pub fn new_store_with_config(
name: String,
config: StoreConfig,
) -> SessionStore
Create a new LocalEts session store with explicit lifetimes.
pub fn registry_running() -> Bool
Whether the registry scope is running on this node.
pub fn registry_supervised() -> supervision.ChildSpecification(
Nil,
)
A child specification that starts the registry scope linked to a
supervisor. The root supervision tree uses it. Starting fails when the
scope already runs (for example after ensure_registry).
pub fn revocation_reason_to_string(
reason: RevocationReason,
) -> String
Stable text for a revocation reason (sent to bound processes).
pub fn rotate(
store: SessionStore,
old_id: String,
) -> Result(Session, SessionStoreError)
Replace a session id with a fresh one carrying the same data.
The old id is deleted first-come-first-served: if two requests rotate the
same id concurrently exactly one succeeds and the other gets
SessionNotFound. Bound WebSockets on the old id are told to close; they
reconnect with the new cookie.
pub fn secure_compare(a: String, b: String) -> Bool
Constant-time string comparison for secrets such as CSRF tokens.
pub fn set(
store: SessionStore,
session: Session,
key: String,
value: String,
) -> Result(Session, SessionStoreError)
Atomically set one key in a session.
The write is applied to the CURRENT stored data (not the possibly stale
session argument), so concurrent set calls on different keys never
lose each other’s writes. A deleted or expired session is never
recreated: the error is returned instead.
pub fn start_store(
backend: SessionBackend,
config: StoreConfig,
) -> Result(SessionStore, SessionStoreError)
Create a session store on any backend. The calling process owns it: a LocalEts table and the maintenance process live as long as it does.
pub fn store_config(store: SessionStore) -> StoreConfig
The lifetime configuration of a store.
pub fn store_error_to_string(err: SessionStoreError) -> String
Stable text for a store error.
pub fn sweep_expired(
store: SessionStore,
) -> Result(Int, SessionStoreError)
Delete every expired session and notify their bound sockets.
Returns how many sessions were removed. Runs automatically every
sweep_interval_ms; exposed for tests and manual maintenance.
pub fn try_create_with_data(
store: SessionStore,
data: dict.Dict(String, String),
) -> Result(Session, SessionStoreError)
Create a new session, returning backend failures.
pub fn try_delete(
store: SessionStore,
id: String,
) -> Result(Nil, SessionStoreError)
Delete a session (logout), returning backend failures. Bound WebSockets are told to close even when the backend fails, so a failed logout never leaves a live socket behind; the session itself may still exist then.
pub fn update(
store: SessionStore,
id: String,
transform: fn(dict.Dict(String, String)) -> dict.Dict(
String,
String,
),
) -> Result(Session, SessionStoreError)
Atomically apply transform to a session’s data.
transform may run more than once if another writer wins the race, so it
must be pure.