auth/session_queries.ts

Auth session database queries.

Server-side sessions keyed by blake3 hash of the session token. The cookie contains the raw token; the database stores only the hash.

view source

Declarations
#

13 declarations

AUTH_SESSION_COLUMNS
#

auth/session_queries.ts view source

readonly ["id", "account_id", "created_at", "expires_at"] import {AUTH_SESSION_COLUMNS} from '@fuzdev/fuz_app/auth/session_queries.js';

The full auth_session column set, named explicitly so a row read fails loud on schema drift — SELECT * would silently carry a dropped or leftover column into the strict-validated wire shapes (see ACCOUNT_COLUMNS in auth/account_queries.ts for the outage class this discipline exists to prevent; the Rust twin already names columns at every session site). Keep in sync with AuthSession and the migration chain's end state — not the frozen v0 DDL in auth/auth_ddl.ts, which still creates last_seen_at for the appended drop migration to remove.

AUTH_SESSION_LIFETIME_MS
#

auth/session_queries.ts view source

number import {AUTH_SESSION_LIFETIME_MS} from '@fuzdev/fuz_app/auth/session_queries.js';

Session lifetime in milliseconds (30 days).

An absolute cap: expires_at is set once at mint and never extended — there is deliberately no touch/renewal query on either spine (a sliding window renews a leaked cookie forever; see docs/security.md §Session Security). The cookie's SESSION_AGE_MAX mirrors this value.

generate_session_token
#

auth/session_queries.ts view source

(): string import {generate_session_token} from '@fuzdev/fuz_app/auth/session_queries.js';

Generate a cryptographically random session token.

returns

string

a 32-byte base64url-encoded token

hash_session_token
#

auth/session_queries.ts view source

(token: string): string & $brand<"SessionId"> import {hash_session_token} from '@fuzdev/fuz_app/auth/session_queries.js';

Hash a session token to its storage key using blake3.

The sole minting point for SessionIdhash_blake3 returns bare hex, so the brand is applied here, where the value gains its meaning.

token

the raw session token

type string

returns

string & $brand<"SessionId">

hex-encoded blake3 hash

query_create_session
#

auth/session_queries.ts view source

(deps: QueryDeps, token_hash: string, account_id: string, expires_at: Date): Promise<void> import {query_create_session} from '@fuzdev/fuz_app/auth/session_queries.js';

Create a new auth session.

deps

query dependencies

token_hash

blake3 hash of the session token (use hash_session_token)

type string

account_id

the account this session belongs to

type string

expires_at

when the session expires

type Date

returns

Promise<void>

query_session_cleanup_expired
#

auth/session_queries.ts view source

(deps: QueryDeps): Promise<number> import {query_session_cleanup_expired} from '@fuzdev/fuz_app/auth/session_queries.js';

Delete expired sessions.

deps

returns

Promise<number>

the number of sessions cleaned up

query_session_enforce_limit
#

auth/session_queries.ts view source

(deps: QueryDeps, account_id: string, max_sessions: number): Promise<number> import {query_session_enforce_limit} from '@fuzdev/fuz_app/auth/session_queries.js';

Enforce a per-account session limit by evicting the oldest sessions.

Keeps the newest max_sessions sessions and deletes the rest.

Race safety: this function must run inside a transaction alongside the INSERT that created the new session. All callers satisfy this requirement:

  • POST /login uses the default transaction: true (framework-managed transaction wrapping in apply_route_specs)
  • The account_token_create RPC handler runs under the dispatcher's transaction path because its spec declares side_effects: true
  • POST /bootstrap and POST /signup manage their own transactions and pass the transaction-scoped deps to create_session_and_set_cookie

The transaction makes one creator's INSERT + enforce_limit pair atomic, but it does not serialize concurrent creators. Under Read Committed, two transactions can't see each other's uncommitted session row, so each computes its OFFSET eviction against a stale count, each preserves its own row, and both commit above max_sessions. A transaction is necessary here but not sufficient. Closing this needs one serialization point per (account_id, credential_kind) — a locked parent row or a transaction-scoped advisory lock — taken before count/evict/insert.

Ordering is created_at DESC, id DESC. The id leg is a stability tie-breaker, not a recency one: created_at defaults to NOW(), the *transaction* timestamp, so two sessions born in one transaction — or in two transactions that started in the same microsecond — tie, and an untied OFFSET may keep a different set on two evaluations of the same rows. id is the blake3 token hash, so the tie-break is arbitrary but deterministic; it makes the survivors reproducible, it does not make the row just inserted a guaranteed survivor. Only the serialization point above does that.

Expired-but-unreaped rows count toward the cap (the predicate is account_id alone). Matches the Rust twin query_session_enforce_limit.

deps

query dependencies (must be transaction-scoped)

account_id

the account to enforce the limit for

type string

max_sessions

maximum number of sessions to keep

type number

returns

Promise<number>

the number of sessions evicted

query_session_get_valid
#

auth/session_queries.ts view source

(deps: QueryDeps, token_hash: string): Promise<{ id: string & $brand<"SessionId">; account_id: string & $brand<"Uuid">; created_at: string; expires_at: string; } | undefined> import {query_session_get_valid} from '@fuzdev/fuz_app/auth/session_queries.js';

Get a session if it exists, is not expired, and has not been revoked.

deps

query dependencies

token_hash

blake3 hash of the session token

type string

returns

Promise<{ id: string & $brand<"SessionId">; account_id: string & $brand<"Uuid">; created_at: string; expires_at: string; } | undefined>

query_session_list_all_active
#

auth/session_queries.ts view source

(deps: QueryDeps, limit?: number): Promise<({ id: string & $brand<"SessionId">; account_id: string & $brand<"Uuid">; created_at: string; expires_at: string; } & { ...; })[]> import {query_session_list_all_active} from '@fuzdev/fuz_app/auth/session_queries.js';

List all active sessions across all accounts with usernames.

Ordered by created_at DESC (newest session first), matching the per-account listing.

deps

query dependencies

limit

maximum entries to return

type number
default 200

returns

Promise<({ id: string & $brand<"SessionId">; account_id: string & $brand<"Uuid">; created_at: string; expires_at: string; } & { username: string; })[]>

active sessions joined with account usernames, newest first

query_session_list_for_account
#

auth/session_queries.ts view source

(deps: QueryDeps, account_id: string, limit?: number): Promise<{ id: string & $brand<"SessionId">; account_id: string & $brand<"Uuid">; created_at: string; expires_at: string; }[]> import {query_session_list_for_account} from '@fuzdev/fuz_app/auth/session_queries.js';

List sessions for an account, newest first.

deps

account_id

type string

limit

type number
default 50

returns

Promise<{ id: string & $brand<"SessionId">; account_id: string & $brand<"Uuid">; created_at: string; expires_at: string; }[]>

query_session_revoke_all_for_account
#

auth/session_queries.ts view source

(deps: QueryDeps, account_id: string): Promise<number> import {query_session_revoke_all_for_account} from '@fuzdev/fuz_app/auth/session_queries.js';

Revoke all sessions for an account.

deps

account_id

type string

returns

Promise<number>

the number of sessions revoked

query_session_revoke_by_hash_unscoped
#

auth/session_queries.ts view source

(deps: QueryDeps, token_hash: string): Promise<void> import {query_session_revoke_by_hash_unscoped} from '@fuzdev/fuz_app/auth/session_queries.js';

Revoke (delete) a session by its token hash, with no account scoping.

The _unscoped suffix is the safety signal — there is no account_id constraint, so callers must guarantee the hash came from a trusted source (the authenticated session cookie path is the only safe production caller — see auth/account_routes.ts /logout). For user-facing revocation of a specific session by ID, use query_session_revoke_for_account (IDOR-guarded).

deps

token_hash

type string

returns

Promise<void>

query_session_revoke_for_account
#

auth/session_queries.ts view source

(deps: QueryDeps, token_hash: string, account_id: string): Promise<boolean> import {query_session_revoke_for_account} from '@fuzdev/fuz_app/auth/session_queries.js';

Revoke a session only if it belongs to the specified account.

Prevents cross-account session revocation.

deps

query dependencies

token_hash

blake3 hash of the session token

type string

account_id

the account that must own the session

type string

returns

Promise<boolean>

true if a session was revoked, false if not found or wrong account

Depends on
#

Imported by
#