auth/account_schema.ts

Auth entity types and client-safe schemas.

Defines the runtime types for the fuz identity system: Account, Actor, RoleGrant, AuthSession, and ApiToken.

Identifier primitives (Username, UsernameProvided, Email) live in primitive_schemas.ts — they're general validator shapes that don't depend on the auth domain. The auth-shape request-contract primitive ActingActor lives in http/auth_shape.ts next to RouteAuth (the two pair: auth.actor !== 'none' ⟺ input declares acting?: ActingActor).

DDL lives in auth/auth_ddl.ts; role system in auth/role_schema.ts. See docs/identity.md for design rationale.

view source

Declarations
#

22 declarations

Account
#

auth/account_schema.ts view source

Account import type {Account} from '@fuzdev/fuz_app/auth/account_schema.js';

Account — authentication identity. You log in as an account.

id

type Uuid

username

type string

email

type string | null

email_verified

type boolean

password_hash

type string

created_at

type string

created_by

type Uuid | null

updated_at

type string

updated_by

type Uuid | null

deleted_at

Soft-delete tombstone. Non-null means the account is deleted (delete = soft); auth resolution treats it as absent. A hard purge removes the row entirely. See auth/account_queries.ts.

type string | null

deleted_by

Actor that performed the soft-delete (initiator: self / admin / keeper). Paired with deleted_at, mirroring role_grant's revoked_at / revoked_by. Plain UUID (no FK, like created_by / updated_by on this table).

type Uuid | null

Actor
#

auth/account_schema.ts view source

Actor import type {Actor} from '@fuzdev/fuz_app/auth/account_schema.js';

Actor — the entity that acts. Owns cells, holds role_grants, appears in audit trails.

id

type Uuid

account_id

type Uuid

name

type string

created_at

type string

updated_at

type string | null

updated_by

type Uuid | null

deleted_at

Soft-delete tombstone — set alongside the owning account's soft-delete.

type string | null

deleted_by

Actor that performed the soft-delete. Paired with deleted_at.

type Uuid | null

ActorSummaryJson
#

auth/account_schema.ts view source

value + type

{ id: string & $brand<"Uuid">; name: string; } import {ActorSummaryJson} from '@fuzdev/fuz_app/auth/account_schema.js';

Zod schema for the actor summary returned in admin account listings.

id

type string & $brand<"Uuid">

name

type string

AdminAccountEntryJson
#

auth/account_schema.ts view source

value + type

{ account: { id: string & $brand<"Uuid">; username: string; email: string | null; email_verified: boolean; created_at: string; updated_at: string; updated_by: (string & $brand<"Uuid">) | null; deleted_at: string | null; }; actor: { ...; } | null; role_grants: { ...; }[]; pending_offers: { ...; }[]; } import {AdminAccountEntryJson} from '@fuzdev/fuz_app/auth/account_schema.js';

Zod schema for an admin account listing entry (account + actor + role_grants + pending offers).

account

type AdminAccountJson

actor

type ActorSummaryJson | null

role_grants

type RoleGrantSummaryJson[]

pending_offers

type PendingOfferSummaryJson[]

AdminAccountJson
#

auth/account_schema.ts view source

value + type

{ id: string & $brand<"Uuid">; username: string; email: string | null; email_verified: boolean; created_at: string; updated_at: string; updated_by: (string & $brand<"Uuid">) | null; deleted_at: string | null; } import {AdminAccountJson} from '@fuzdev/fuz_app/auth/account_schema.js';

Zod schema for admin-facing account data — extends SessionAccountJson with audit fields.

id

type string & $brand<"Uuid">

username

type string

email

type string | null

email_verified

type boolean

created_at

type string

updated_at

type string

updated_by

type (string & $brand<"Uuid">) | null

deleted_at

Soft-delete tombstone, non-null when the account is deleted. Surfaced so the admin UI can mark tombstoned rows (shown only when the listing is requested with include_deleted) and offer reactivation via account_undelete. Active listings always carry null here.

type string | null

ApiToken
#

auth/account_schema.ts view source

ApiToken import type {ApiToken} from '@fuzdev/fuz_app/auth/account_schema.js';

API token for CLI/programmatic access.

id

type string

account_id

type Uuid

name

type string

token_hash

type string

expires_at

type string | null

last_used_at

type string | null

last_used_ip

type string | null

created_at

type string

scope

The token's authority narrowing (api_token.scope, NOT NULL). Stored as JSONB; read back through parse_token_scope, which is fail-closed — an unreadable document refuses the credential rather than widening it.

type unknown

AuthSession
#

auth/account_schema.ts view source

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

type AuthSessionJson

import type {AuthSession} from '@fuzdev/fuz_app/auth/account_schema.js';

Server-side auth session row, keyed by blake3 hash of the session token. Structurally identical to the wire shape AuthSessionJson — kept as the DB-row name so query signatures keep saying which side of the boundary they read.

id

type string & $brand<"SessionId">

account_id

type string & $brand<"Uuid">

created_at

type string

expires_at

type string

AuthSessionJson
#

auth/account_schema.ts view source

value + type

{ id: string & $brand<"SessionId">; account_id: string & $brand<"Uuid">; created_at: string; expires_at: string; } import {AuthSessionJson} from '@fuzdev/fuz_app/auth/account_schema.js';

Zod schema for AuthSession — id is the blake3 hash, safe for client.

id

type string & $brand<"SessionId">

account_id

type string & $brand<"Uuid">

created_at

type string

expires_at

type string

ClientApiTokenJson
#

auth/account_schema.ts view source

value + type

{ id: string; account_id: string & $brand<"Uuid">; name: string; expires_at: string | null; last_used_at: string | null; last_used_ip: string | null; created_at: string; scope: string; } import {ClientApiTokenJson} from '@fuzdev/fuz_app/auth/account_schema.js';

Zod schema for client-safe API token listing (excludes token_hash).

id

type string

account_id

type string & $brand<"Uuid">

name

type string

expires_at

type string | null

last_used_at

type string | null

last_used_ip

type string | null

created_at

type string

scope

Human-readable scope label (full / full (grandfathered) / N methods). A label rather than the document so the list stays a display surface — nothing authorizes off this, and a grandfathered token reads differently from a deliberately-full one.

type string

CreateAccountInput
#

auth/account_schema.ts view source

CreateAccountInput import type {CreateAccountInput} from '@fuzdev/fuz_app/auth/account_schema.js';

username

type string

password_hash

type string

email?

type string | null

CreateRoleGrantInput
#

auth/account_schema.ts view source

CreateRoleGrantInput import type {CreateRoleGrantInput} from '@fuzdev/fuz_app/auth/account_schema.js';

actor_id

type Uuid

role

type string

scope_kind?

Machine-readable kind for the scope_id. Required iff scope_id is set; must be null/omitted when scope_id is null. The DB-level role_grant_scope_kind_paired CHECK rejects mismatched pairs.

type string | null

scope_id?

Scope the grant applies to. null / omitted grants a global role_grant.

type Uuid | null

expires_at?

type Date | null

granted_by

type Uuid | null

source_offer_id?

Offer id that produced this role_grant. Set by query_accept_offer; leave unset for direct grants.

type Uuid | null

has_global_role
#

auth/account_schema.ts view source

(role_grants: readonly { role: string; scope_id: string | null; revoked_at?: string | null | undefined; expires_at: string | null; }[], role: string, now?: Date): boolean import {has_global_role} from '@fuzdev/fuz_app/auth/account_schema.js';

Whether role_grants holds a live global grant of role — scope_id is null and the grant passes is_role_grant_active.

This is the question a role gate asks. A scoped grant confers its role on one resource only, and a scoped grant of a builtin role (admin, keeper) confers nothing, so matching on the role name alone would read a grant for one resource as authority everywhere. Server-side the same rule is has_scoped_role(ctx, role, null) over a request context and query_account_has_global_role in SQL; this is the form for a bare list of grants — AuthState.role_grants, an admin listing row.

The scope match is scope_id === null, so a grant decoded without the field is not read as global. Client-side the answer shapes the UI and nothing else: the server re-checks every request.

role_grants

the grants to search, e.g. AuthState.role_grants

type readonly { role: string; scope_id: string | null; revoked_at?: string | null | undefined; expires_at: string | null; }[]

role

the role to check

type string

now

current time (defaults to new Date(), pass for testability)

type Date
default new Date()

returns

boolean

true iff a live grant of role with no scope is present

is_role_grant_active
#

auth/account_schema.ts view source

(p: { revoked_at?: string | null | undefined; expires_at: string | null; }, now?: Date): boolean import {is_role_grant_active} from '@fuzdev/fuz_app/auth/account_schema.js';

Whether a role grant is live, from its wire fields — not revoked, and not past expires_at.

Server-side this is belt-and-suspenders: every role_grant a request context holds came through query_role_grant_find_active_for_actor, whose revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW()) is the authoritative gate on both spines (the Rust twin's in-memory RoleGrantRow carries no timestamps at all and re-checks nothing).

expires_at arrives second-truncated, so the expiry check is is_iso8601_seconds_live (timestamp.ts): it admits through the end of the second the stamp names, so the predicate can never report a grant expired *before* the SQL gate would — a bare expires_at > now would deny an admitted grant for up to a second.

p

the grant's revocation + expiry fields

type { revoked_at?: string | null | undefined; expires_at: string | null; }

now

current time (defaults to new Date(), pass for testability and hot-path efficiency)

type Date
default new Date()

returns

boolean

true when the grant is neither revoked nor expired

PendingOfferSummaryJson
#

auth/account_schema.ts view source

value + type

{ id: string & $brand<"Uuid">; role: string; scope_kind: string | null; scope_id: (string & $brand<"Uuid">) | null; from_actor_id: string & $brand<"Uuid">; from_username: string; created_at: string; expires_at: string; } import {PendingOfferSummaryJson} from '@fuzdev/fuz_app/auth/account_schema.js';

Zod schema for a pending role_grant offer surfaced in admin account listings.

Deliberately narrower than RoleGrantOfferJson: omits message and decline_reason so cross-admin visibility of the listing does not expose grantor-authored text that the audit log also withholds. Full offer payloads remain available through the offer-specific RPC surface and the audit log when admins need them.

from_username is resolved server-side so multi-admin deployments can see at a glance whose pending offer is blocking a "+ {role}" button; the resolution runs inside the listing query's parallel batch.

id

type string & $brand<"Uuid">

role

type string

scope_kind

type string | null

scope_id

type (string & $brand<"Uuid">) | null

from_actor_id

type string & $brand<"Uuid">

from_username

type string

created_at

type string

expires_at

type string

ROLE_GRANT_REVOKED_REASON_LENGTH_MAX
#

auth/account_schema.ts view source

500 import {ROLE_GRANT_REVOKED_REASON_LENGTH_MAX} from '@fuzdev/fuz_app/auth/account_schema.js';

Maximum length of the optional free-form revoked_reason attached to a revoked role_grant. Bounds the value at the schema layer so both the admin input (when the route surfaces a reason field) and the revokee-facing role_grant_revoke WS notification validate against the same ceiling. Counted in UTF-16 code units.

RoleGrant
#

auth/account_schema.ts view source

RoleGrant import type {RoleGrant} from '@fuzdev/fuz_app/auth/account_schema.js';

Role grant — time-bounded, revocable grant of a role to an actor.

id

type Uuid

actor_id

type Uuid

role

type string

scope_kind

Machine-readable kind tag for the polymorphic scope_id. Paired-null with scope_id per the role_grant_scope_kind_paired CHECK: both null (global) or both non-null (scoped). Consumer-declared via create_scope_kind_schema(...); v1 keeps validation registry-membership only, with no INSERT-time (role, scope_kind) enforcement.

type string | null

scope_id

Resource scope this grant applies to (e.g. a classroom id). null for global role_grants.

type Uuid | null

created_at

type string

expires_at

type string | null

revoked_at

type string | null

revoked_by

type Uuid | null

revoked_reason

Optional free-form reason attached on revoke (rides on the role_grant_revoke WS notification to the revokee).

type string | null

granted_by

type Uuid | null

source_offer_id

Offer that produced this role_grant (set by query_accept_offer). null for direct grants.

type Uuid | null

RoleGrantSummaryJson
#

auth/account_schema.ts view source

value + type

{ id: string & $brand<"Uuid">; role: string; scope_kind: string | null; scope_id: (string & $brand<"Uuid">) | null; created_at: string; expires_at: string | null; granted_by: (string & $brand<...>) | null; } import {RoleGrantSummaryJson} from '@fuzdev/fuz_app/auth/account_schema.js';

Zod schema for the role_grant summary returned in admin account listings.

id

type string & $brand<"Uuid">

role

type string

scope_kind

type string | null

scope_id

type (string & $brand<"Uuid">) | null

created_at

type string

expires_at

type string | null

granted_by

type (string & $brand<"Uuid">) | null

SessionAccount
#

auth/account_schema.ts view source

SessionAccount import type {SessionAccount} from '@fuzdev/fuz_app/auth/account_schema.js';

Account without sensitive fields, scoped to the authenticated user's own session.

id

type Uuid

username

type string

email

type string | null

email_verified

type boolean

created_at

type string

SessionAccountJson
#

auth/account_schema.ts view source

value + type

{ id: string & $brand<"Uuid">; username: string; email: string | null; email_verified: boolean; created_at: string; } import {SessionAccountJson} from '@fuzdev/fuz_app/auth/account_schema.js';

Zod schema for SessionAccount — account without sensitive fields.

id

type string & $brand<"Uuid">

username

type string

email

type string | null

email_verified

type boolean

created_at

type string

SessionId
#

auth/account_schema.ts view source

value + type

string & $brand<"SessionId">

type string & $brand<"SessionId">

import {SessionId} from '@fuzdev/fuz_app/auth/account_schema.js';

A session's storage key — the blake3 hash of its raw token, branded so a bare string can't stand in for one. Blake3Hash (primitive_schemas.ts) is the shape; this is the meaning. Mint only via hash_session_token, or SessionId.parse at an external boundary.

The raw token itself is never typed — it stays a string that exists only long enough to be hashed.

external types

z.infer<typeof SessionId>

to_admin_account
#

auth/account_schema.ts view source

(account: Account): { id: string & $brand<"Uuid">; username: string; email: string | null; email_verified: boolean; created_at: string; updated_at: string; updated_by: (string & $brand<...>) | null; deleted_at: string | null; } import {to_admin_account} from '@fuzdev/fuz_app/auth/account_schema.js';

Convert an Account to an AdminAccountJson for admin listings.

account

the full account record

type Account

returns

AdminAccountJson

the admin-safe account with audit fields

to_session_account
#

auth/account_schema.ts view source

(account: Account): SessionAccount import {to_session_account} from '@fuzdev/fuz_app/auth/account_schema.js';

Convert an Account to a SessionAccount by stripping sensitive fields.

account

the full account record

type Account

returns

SessionAccount

the client-safe account

Depends on
#

Imported by
#