auth/resolved_auth.ts

The account-grain credential a request resolved to, and the re-check a long-lived connection runs on it once it is registered.

The auth middleware chain (daemon token → session cookie → bearer) resolves a credential and leaves it on the Hono context as separate keys. ResolvedAuth is that credential gathered into one value (get_resolved_auth), so the connections that outlive their request — a WebSocket, an SSE stream — can carry it past the handshake, re-read it with revalidate_resolved_auth, and close at its credential_deadline.

view source

Declarations
#

3 declarations

get_resolved_auth
#

auth/resolved_auth.ts view source

(c: Context<any, any, {}>): ResolvedAuth | null import {get_resolved_auth} from '@fuzdev/fuz_app/auth/resolved_auth.js';

Gather the credential the auth middleware resolved for this request.

Reads the keys the session, bearer, and daemon-token middleware set and keeps only the one that belongs to the winning credential type — a daemon token overrides a session cookie sent alongside it without clearing that session's hash, so the hash is carried for a session credential alone.

c

the request context, after the auth middleware chain

type Context<any, any, {}>

returns

ResolvedAuth | null

the resolved credential, or null for an unauthenticated request

ResolvedAuth
#

auth/resolved_auth.ts view source

ResolvedAuth import type {ResolvedAuth} from '@fuzdev/fuz_app/auth/resolved_auth.js';

An authenticated request's credential, account-grain — no actor.

token_hash is set for a session, api_token_id for an API token, and both are null for a daemon token. The twin of the Rust spine's ResolvedAuth, which carries the account row and token scope as well; here those stay on the Hono context.

account_id

The authenticated account.

type string

credential_type

How the request was authenticated.

type CredentialType

token_hash

Blake3 hash of the session token, or null for non-session credentials.

type string | null

api_token_id

api_token.id for a bearer credential, else null.

type string | null

credential_deadline

When the credential stops authenticating, as a monotonic performance.now() timestamp — the moment a long-lived connection opened on it is closed (schedule_credential_deadline), since expiry writes nothing a revocation listener could hear. The earliest of what the credential carries: a session's cookie and row expiry, an API token's row expiry (null for an eternal one); null for the daemon token. A row's expiry is read as time remaining on the database clock (auth/credential_deadline.ts). The Rust twin's credential_deadline.

type number | null

revalidate_resolved_auth
#

auth/resolved_auth.ts view source

(deps: QueryDeps, resolved: ResolvedAuth): Promise<boolean> import {revalidate_resolved_auth} from '@fuzdev/fuz_app/auth/resolved_auth.js';

Re-check that a resolved credential still authenticates: true when it does, false when it has been revoked, has expired, or its account is gone.

For a connection that outlives its request — a WebSocket, an SSE stream — and is authorized once, at open. Such a connection is closed on revocation by identity, but only once it is *registered*; between the credential read and the registration a revocation closes nothing. So the opener registers first (un-admitted — BackendWebsocketTransport.register_pending / SubscriberRegistry.subscribe_pending), then calls this, then admits. A revocation that committed before this read is seen by it; one whose close ran after the registration found the pending entry.

That needs each statement here to see everything committed before it started, so deps.db must be the pool-level Db (autocommit) or a READ COMMITTED transaction — never a REPEATABLE READ / SERIALIZABLE transaction opened before the registration, whose snapshot would predate it.

What is re-read, per credential — each through the same predicate the resolve path used, so the two cannot disagree about what "valid" means (the session and account legs call the middleware's own queries; the token leg shares its WHERE fragment):

  • Session — the auth_session row still exists, is unexpired, and belongs to the resolved account.
  • API token — the api_token row still exists, is unexpired, and belongs to the resolved account (by id; the raw token is not kept).
  • Daemon token — nothing of its own. The token has no row, and no revocation event: rotation closes no connection, so there is no close an opening connection could have missed. Its keeper account is still re-read.
  • Every credential — the account row still exists and is not soft-deleted.

It re-checks the credential, not what a revocation chose to close: a logout closes every connection of the account but deletes only its own session, so another session's connection opening concurrently passes this check and is refused only if that account-wide close found its pending registration — the same two outcomes an already-open connection has.

No side effects. Unlike the bearer middleware this never touches api_token.last_used_at / last_used_ip — re-checking a credential is not using it — and it writes nothing else.

A ResolvedAuth whose credential type lacks its own key (a session with no token_hash, a bearer with no api_token_id) is a shape the middleware never builds; it is answered false.

The twin of the Rust spine's revalidate_resolved_auth.

deps

query dependencies; the pool-level Db, not a snapshot transaction

resolved

the credential the request authenticated with

returns

Promise<boolean>

whether the credential still authenticates

throws

  • Error - propagated from the database. The caller must fail closed — an unchecked credential is not a valid one.

Depends on
#

Imported by
#