auth

59 modules

  • auth/account_action_specs.ts

    Account RPC action specs β€” declarative contract for self-service account operations. Import this module for the specs, Input/Output schemas, and the all_account_action_specs registry. Handlers live in auth/account_actions.ts so consumers doing typed-client codegen or surface reporting don't transitively drag in server-only query code.

  • auth/account_actions.ts

    Account RPC action handlers β€” self-service operations for the authenticated account.

    Seven request_response actions bound to handlers:

    - Session reads: account_verify, account_session_list. - Session mutations: account_session_revoke, account_session_revoke_all. - API token management: account_token_create, account_token_list, account_token_revoke.

    The action specs themselves live in auth/account_action_specs.ts. Every spec declares auth: {account: 'required', actor: 'none'} so the dispatcher enforces account-grain auth before the handler runs. Revoke operations are account-scoped (via query_session_revoke_for_account / query_revoke_api_token_for_account) so passing another account's session or token id returns revoked: false rather than revealing whether the id exists.

    Counterpart to auth/account_routes.ts, which keeps the cookie-lifecycle flows (login, logout, password, signup, bootstrap) on REST.

  • auth/account_queries.ts

    Account and actor database queries.

    Provides CRUD operations for the account and actor tables. For v1, every account has exactly one actor (1:1).

  • auth/account_routes.ts

    Account route specs for cookie-based session management.

    Returns RouteSpec[] β€” caller applies them to Hono via apply_route_specs.

    Four REST flows remain here; each has a concrete reason to stay REST rather than moving to auth/account_actions.ts:

    - POST /login β€” issues a signed Set-Cookie and pre-handler rate-limits by IP + per-canonical-account before password hashing. - POST /logout β€” clears the session cookie. - POST /password β€” cookie clear + revoke-all cascade; rate-limit-shaped error envelope on 429. - GET /verify β€” empty-body nginx auth_request probe. Programmatic callers should use the account_verify RPC action for the typed payload.

    Session listing/revocation and API token CRUD are on the RPC endpoint β€” see auth/account_actions.ts. Signup is in auth/signup_routes.ts. Defaults are closed/safe: accounts are created through bootstrap, admin action, or invite.

  • 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.

  • auth/actor_lookup_action_specs.ts

    actor_lookup RPC spec β€” authenticated batched id β†’ username/display_name resolver, keyed by actor id.

    Powers the labels arc for surfaces that stamp an actor id (bylines, owner columns, grantor labels, audit-log "by" cells). One round trip resolves an array of ids to display strings.

    Auth + rate-limit posture

    {account: 'required', actor: 'none'} + rate_limit: 'account'. Account-grain β€” only that the caller is signed in matters, not which actor is calling, so resolution skips the actor phase. The auth gate + per-account rate limit (default 1200/15min) + the {@link ACTOR_LOOKUP_IDS_MAX | per-call cap} bound the batched username-enumeration surface that the cell_list ↔ actor_lookup pair would otherwise present.

    If a public-surface byline ever lands (e.g. an unauthenticated gallery), it should resolve via a separate public-safe mechanism (SSR-stamped labels or a per-cell embedded actor label), not by loosening this gate.

    Wire shape β€” info-leak audit

    Output: {actors: [{id, username, display_name?}]}. Deliberately omitted:

    - account_id β€” the actor↔account join is a control-plane detail - email, password/credential fields β€” never queried - created_at / updated_at β€” timing-oracle avoidance - role / role_grants / session state β€” separation of concern

    display_name is omitted (not null) when actor.name is blank, so clients see undefined rather than a sentinel string. Unknown ids are silently absent from the response β€” by construction this is an existence-oracle (the caller can diff response ids against request ids), bounded by:

    1. rate-limit (per-account, see above), 2. {@link ACTOR_LOOKUP_IDS_MAX} cap per call, 3. actor-uuid intractability (122-bit random), 4. hard-deleted actors are indistinguishable from never-existed (no tombstone oracle β€” see actor_lookup_queries.ts).

    Response order is unspecified β€” callers index by id when needed.

  • auth/actor_lookup_actions.ts

    actor_lookup RPC handler.

    Pure read β€” no audit, no side effects. Auth (account: 'required') + rate-limit (account-grain) enforced at the spec layer; see auth/actor_lookup_action_specs.ts for the info-leak audit.

    display_name is omitted (not null) when actor.name is blank, matching the wire shape display_name? so the typed client sees an undefined rather than a sentinel string.

  • auth/actor_lookup_queries.ts

    Batched actor-by-id resolver.

    Joins actor ⨝ account so callers see (username, display_name) for each actor row. The byline / owner-column / grantor surfaces stamp an actor id, so resolving "who is this actor?" lands the human label in one round trip.

    Accounts may host multiple actors (multi-actor shipped in v0.55.0). The inner join still resolves one row per actor β€” actor.account_id is NOT NULL so every actor has exactly one account.

    Info-leak posture (see actor_lookup_action_specs.ts Β§audit):

    - Row shape omits account_id β€” the join is control-plane, not wire-visible. - Hard-deleted actors (or account-cascade-orphaned rows) drop out silently β€” indistinguishable from never-existed (no tombstone oracle). - No created_at / updated_at projected (timing-oracle avoidance). - Response order is unspecified β€” WHERE id = ANY(...) returns index-scan order in practice but callers must not depend on it.

    Caller is responsible for capping ids.length β€” the SQL itself does not enforce a bound; the action-spec layer surfaces invalid_params via ACTOR_LOOKUP_IDS_MAX.

  • auth/actor_search_action_specs.ts

    actor_search RPC spec β€” authenticated case-insensitive prefix search over actor.name, returning the same {id, username, display_name?} wire shape as actor_lookup.

    Powers person-target pickers β€” visiones' CellGrantsEditor.svelte teacher-picks-student flow replaces the deferred actor_by_name arm of cell_grant_create with a debounced search against this method. Sibling to actor_lookup: that resolves a known batch of ids β†’ labels; this resolves a partial name β†’ candidate actors.

    Auth + rate-limit posture

    {account: 'required', actor: 'none'} + rate_limit: 'account'. Same shape as actor_lookup: only that the caller is signed in matters, not which actor is calling. The auth gate, the per-account rate limit (default 1200/15min), and the ACTOR_SEARCH_LIMIT_MAX per-call cap bound the enumeration surface this method would otherwise present.

    The handler additionally requires the caller to be admin when scope_ids is empty (the unbounded global-search arm). Non-admin callers must always pass at least one scope_id β€” the SQL filters actors to those holding a role_grant on one of the supplied scopes, so a non-admin caller is restricted to actors they share a scope with. The admin check is account-grain (any actor on the caller's account holds a global admin role_grant), matching the actor: 'none' posture.

    Caller-passes-scope_ids design

    scope_ids is trusted as a filter, not as an authority claim β€” the SQL filters to actors with role_grants on those scopes regardless of whether the caller has authority over them. Consumers are responsible for pre-filtering scope_ids against their own authority before calling. Visiones passes the set of classrooms the teacher teaches, sourced client-side from the teacher's role_grant list; the teacher predicate stays in the visiones layer rather than baked into fuz_app.

    Crucially, this does not widen the scope-existence oracle: an attacker passing a random scope_id cannot learn "this scope has members matching X" because the join filters to actors holding a role_grant on the scope, and the SQL surfaces neither "did the scope exist" nor "did the scope have non-matching members" β€” only the matching subset is returned.

    Wire shape β€” info-leak audit

    Output {actors: [{id, username, display_name?}]} is identical to actor_lookup's β€” see auth/actor_lookup_action_specs.ts for the full field-by-field audit. Same omissions (account_id, email, timestamps, role / role_grants / session state), same display_name omitted-not-null contract, same response-order-unspecified rule.

    Additional actor_search-specific posture:

    - Prefix match (LOWER(name) LIKE LOWER(query) || '%'), not full %query%. Full-LIKE would let a single call enumerate one alphabetical bucket spread across many starting letters, which defeats the per-call cap as an enumeration bound. - Hard-deleted actors silently drop (cascade through actor.account_id FK) β€” no tombstone oracle, same posture as actor_lookup. - Empty result set on no-match β€” fail-soft like cell_list. No "no actor matches" error message that would leak an existence boundary on the search-term axis.

    Why not extend actor_lookup?

    Splitting the methods keeps the wire contracts independent: actor_lookup's input is {ids}, actor_search's is {query} + optional filters. Both surface the same ActorLookupEntryJson row shape (re-used here), so the labels arc on the consumer side stays uniform.

  • auth/actor_search_actions.ts

    actor_search RPC handler.

    Pure read β€” no audit, no side effects. Auth (account: 'required', actor: 'none') + rate-limit (account-grain) enforced at the spec layer; see auth/actor_search_action_specs.ts for the info-leak audit and threat model.

    The handler adds two checks the spec layer can't express:

    - Admin gate on empty scope_ids β€” unbounded global search is admin-only. Non-admin callers without a scope_ids filter are rejected with invalid_params carrying actor_search_scope_required. The admin check is account-grain (any actor on the caller's account holds a global admin role_grant) since the actor: 'none' posture doesn't load auth.role_grants for an in-memory check. - Limit clamp β€” input is bounded by ACTOR_SEARCH_LIMIT_MAX at the schema; the handler picks the default when omitted.

    display_name is omitted (not null) when actor.name is blank, matching the wire shape ActorLookupEntryJson.display_name? β€” same convention as actor_lookup_actions.ts.

  • auth/actor_search_queries.ts

    Prefix-based actor search.

    Sibling to actor_lookup_queries.ts β€” that resolves a batch of ids to labels; this resolves a partial name to candidate actors. Same row shape (ActorLookupRow) so the labels arc on the consumer side stays uniform.

    Case-insensitive LIKE-prefix on actor.name backed by the idx_actor_name_lower functional index. LIKE wildcards (%, _, \) in the query string are escaped at the JS layer so the prefix-only contract is enforceable β€” an unescaped %xyz would widen the surface to full-LIKE and defeat the per-call cap as a binding bound.

    Auth filtering β€” scope_ids

    When scope_ids is non-empty, the result is filtered to actors holding an active role_grant on one of the supplied scopes. Active means revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW()). Stale (revoked / expired) role_grants do not confer membership for search-visibility purposes β€” otherwise a removed student would remain visible to teachers indefinitely.

    The DISTINCT on actor.id collapses the case where an actor holds multiple matching role_grants in the supplied scope set into one row.

    When scope_ids is omitted (admin-only global path; the handler gates), no role_grant join β€” every actor with a matching prefix is returned.

    Info-leak posture (see actor_search_action_specs.ts Β§audit)

    - Row shape omits account_id β€” the join is control-plane, not wire-visible. Identical to actor_lookup_queries.ts. - Hard-deleted actors (cascade-orphaned via actor.account_id FK) drop out silently. - No created_at / updated_at projected (timing-oracle avoidance). - Scope-membership uses ANY on the supplied scope_ids array but never surfaces "which scope matched" β€” the result row carries only the actor's wire shape. An attacker passing a random scope_id learns at most "this scope has at least one member matching X" if a match exists, indistinguishable from a no-match search; the caller-passes-scope_ids design (handler trusts the array as a filter, not as authority) means the attacker had to obtain the scope_id from somewhere else first.

    Caller bounds limit (the action-spec layer enforces ACTOR_SEARCH_LIMIT_MAX); SQL clamps to that cap on the call site before reaching this query.

  • auth/admin_action_specs.ts

    Admin RPC action specs β€” declarative contract for admin-only operations.

    Import this module for the specs, Input/Output schemas, and the all_admin_action_specs registry. Handlers live in auth/admin_actions.ts.

    Authorization is declared at the spec level (auth: {role: ROLE_ADMIN}) so the RPC dispatcher enforces admin before the handler runs and the generated surface accurately reports the requirement.

    The registry always includes app_settings_get / app_settings_update β€” the runtime factory only wires their handlers when AdminActionOptions.app_settings is provided; dispatch falls back to method_not_found when absent.

  • auth/admin_actions.ts

    Admin RPC action handlers β€” admin-only operations exposed on the JSON-RPC surface.

    Four action categories:

    - Account management: admin_account_list, admin_session_list, admin_session_revoke_all, admin_token_revoke_all. - Audit log reads: audit_log_list, audit_log_role_grant_history. - Invite CRUD: invite_create, invite_list, invite_delete. - App settings: app_settings_get, app_settings_update (registered only when AdminActionOptions.app_settings is provided β€” the mutable ref is owned by the server context and shared with signup middleware).

    The action specs themselves live in auth/admin_action_specs.ts. Mutations emit matching audit events via deps.audit.emit.

    Authorization is declared at the spec level (auth: {role: 'admin'}) so the RPC dispatcher enforces it before the handler runs and the generated surface accurately reports the requirement. role_grant_revoke in auth/role_grant_offer_actions.ts uses the same spec-level pattern even though its sibling methods are authenticated-but-not-admin β€” the dispatcher checks auth per-spec, so mixed-auth endpoints compose cleanly. Handler-level gates are reserved for input-dependent elevation (e.g. role_grant_offer_list/_history elevate to admin only when the caller passes an account_id other than their own β€” an input-dependent check the spec can't express).

  • auth/all_action_spec_registries.ts

    Canonical list of every fuz_auth action-spec registry β€” for cross-cutting walkers and codegen only. Not a mounting surface; consumers continue to import individual all_*_action_specs bundles (and create_*_actions factories) per registration site.

    The "one main bundle" alternative is an antipattern for mounting: create_standard_rpc_actions (admin + role_grant_offer + account) is the canonical surface, and opt-in registries (self_service_role, actor_lookup) are deliberately opt-in because their eligibility (eligible_roles) or coverage (byline labels) is app-specific. Spreading everything into a single mount would silently widen the dispatch surface the moment a new opt-in landed β€” the exact failure mode this module is built to detect, not propagate. See ./CLAUDE.md Β§RPC actions (standard_rpc_actions.ts).

    Use cases for this registry:

    - Cross-registry walker tests (input-invariants, auth-shape biconditional) β€” iterate the spec arrays once, fail when a new registry slips by without an entry here. - Codegen that needs to see every fuz_auth surface at once (typed-client filters, attack-surface reports). For typed-client wiring of the standard surface, prefer all_standard_action_specs in auth/standard_action_specs.ts β€” it mirrors the create_standard_rpc_actions mount and stays narrower than this registry-of-registries (no opt-in bundles).

    protocol_action_specs (heartbeat / cancel) is not included β€” those are transport-level wire-protocol concerns shipped by fuz_app and spread by every consumer at registration via protocol_actions from actions/protocol.ts. Walker tests that need protocol coverage spread protocol_action_specs separately.

  • auth/api_token_queries.ts

    API token query functions for token CRUD and validation.

  • auth/api_token.ts

    API token generation and hashing utilities.

    Tokens use the format secret_fuz_token_<base64url> and are stored as blake3 hashes. These are pure cryptographic operations with no framework dependency β€” the bearer auth middleware that validates tokens lives in auth/bearer_auth.ts.

  • auth/app_settings_queries.ts

    App settings database queries.

    Single-row table queries for global app configuration.

  • auth/app_settings_schema.ts

    App settings types and client-safe schemas.

    Single-row table for global app configuration (e.g. open signup toggle).

  • auth/audit_emitter.ts

    Bound audit-emit capability.

    AuditEmitter closes over the pool-level Db, the on_audit_event subscriber chain, and the optional AuditLogConfig. Built by the consumer's audit_factory callback on CreateAppBackendOptions β€” create_app_backend invokes the factory once with its constructed {db, log} and lands the result on AppDeps.audit. Consumers reach for deps.audit.emit(ctx, input) and never see the pool β€” handlers cannot accidentally emit an audit event against the request's transactional db (which would be rolled back with the parent on a handler throw).

    Four methods cover every fan-out shape the auth domain needs:

    - emit(ctx, input) β€” fire-and-forget pool write. Pushes the in-flight promise onto ctx.pending_effects for post-response flushing. Errors are logged, never thrown. Returns void so callers don't pile up void keywords or accidentally await something whose handle is already in pending_effects. - emit_role_grant_target(ctx, auth, input) β€” wrapper that lifts the actor_id / account_id / ip boilerplate every role-grant-shape audit site repeated. Delegates to emit. - emit_pool(input) β€” awaitable pool write for code paths without a pending_effects queue (cleanup sweeps, ad-hoc maintenance scripts). Same write-then-notify semantics as emit, just synchronous-with-await. - notify(event) β€” fan out an already-written audit row (e.g. rows returned by query_accept_offer that were inserted in-transaction by the query layer). Runs every listener on the chain; per-listener throws are isolated.

    The chain is a documented mutable seam β€” create_app_server appends additional listeners after the backend is built (the factory-managed audit-log SSE, per-endpoint WS auth guards and logout closers, any extra_audit_handlers on a WsEndpointSpec) before the first request runs. Consumers can also append listeners directly on the emitter they return from audit_factory for setups that don't pass through create_app_server.

  • auth/audit_log_ddl.ts

    Audit log DDL β€” CREATE TABLE + index statements for the audit_log table.

    Consumed by auth/migrations.ts. Separated from auth/audit_log_schema.ts so the schema module stays Zod-only (paired with auth/auth_ddl.ts and auth/role_grant_offer_ddl.ts).

    Multi-actor invariants the envelope columns assume:

    - actor_id + account_id, when both populated, refer to the same account (derivable via actor.account_id). Denormalized for indexed audit queries; do not let them disagree. - target_actor_id + target_account_id, same rule when both populated. - target_account_id is the SSE/WS socket-close key β€” sessions stay account-grain after multi-actor lands, so this column carries the routing identity even on actor-bound events. - target_actor_id is populated iff the event subject is actor-bound (see AuditLogEvent.target_actor_id doc-comment for the rule).

  • auth/audit_log_queries.ts

    Audit log database queries.

    Records and retrieves auth mutation events for security monitoring. The canonical fire-and-forget entry point is AppDeps.audit.emit(ctx, input) (see auth/audit_emitter.ts) β€” it closes over the pool so audit rows persist even when the request transaction rolls back. This module only exposes the in-transaction query_* primitives and the drift counters; the bound emitter writes through query_audit_log against its captured pool.

  • auth/audit_log_routes.ts

    Audit log SSE stream route.

    The two list-reads (audit_log_list, audit_log_role_grant_history) moved to RPC in auth/admin_actions.ts, and the admin session listing moved to admin_session_list on the same file. What remains here is the optional GET /audit/stream SSE route β€” streams aren't an action-kind, so they stay on REST. The event payload broadcast on the stream surfaces via audit_log_event_specs (one EventSpec per audit event type) declared alongside the broadcaster in realtime/sse_auth_guard.ts.

  • auth/audit_log_schema.ts

    Audit log types and client-safe Zod schemas.

    Records auth mutations (login, logout, grant, revoke, etc.) for security monitoring and operational visibility.

    Table DDL and indexes live in auth/audit_log_ddl.ts.

  • auth/auth_ddl.ts

    Identity-table DDL β€” CREATE TABLE, index, and seed statements for the core auth tables (account, actor, role_grant, auth_session, api_token, bootstrap_lock, invite, app_settings).

    Consumed by auth/migrations.ts. Paired with auth/audit_log_ddl.ts (audit table) and auth/role_grant_offer_ddl.ts (offer table) β€” DDL lives in *_ddl.ts, Zod schemas in *_schema.ts.

  • auth/auth_guard_resolver.ts

    Auth guard resolver for the route spec system.

    Maps the four-axis RouteAuth (account / actor / roles / credential_types) to two-phase middleware sets that apply_route_specs weaves into the per-route pipeline:

    - pre_validation runs before input validation. require_auth lands here whenever auth.account === 'required' or `auth.actor === 'required' (per registry-time invariant 3, actor: 'required'` today implies a credential β€” accountless actors are out of scope for v1). Pre-validation 401 fires before any body parsing so unauthenticated callers never see route-shape information from parse failures. - post_authorization runs after the dispatcher's authorization phase has populated RequestContext. require_role(roles) fires whenever auth.roles?.length. require_credential_types(types) fires whenever auth.credential_types?.length.

    Public routes (auth.account === 'none' && auth.actor === 'none') yield empty guard arrays. 'optional' axes contribute no pre-validation 401; the authorization phase sets RequestContext to whatever the credential supports and the post-authorization gates decide whether the actor's role_grants / credential type match.

  • auth/bearer_auth.ts

    Bearer auth middleware for API token authentication.

    Bearer tokens are rejected when Origin or Referer headers are present β€” browsers must use cookie auth. This reduces attack surface: a stolen token cannot be replayed from a browser context (the browser adds Origin automatically).

    Token generation and hashing utilities live in auth/api_token.ts.

  • auth/bootstrap_account.ts

    Bootstrap flow for creating the first account.

    Uses an atomic bootstrap_lock table to prevent TOCTOU race conditions. Token verification and account creation happen in a single transaction.

  • auth/bootstrap_routes.ts

    Bootstrap route spec for first-time account creation.

    One-shot endpoint: exchanges a bootstrap token + credentials for an account with keeper privileges and a session cookie.

  • auth/cleanup.ts

    Periodic auth cleanup β€” sweeps expired sessions and role_grant offers.

    Single entry point for consumers scheduling auth maintenance. Internally runs every known sweep and emits the corresponding audit events so consumer code only manages cadence, not per-task wiring.

    The per-task primitives remain exported from their home modules (query_session_cleanup_expired, query_role_grant_offer_sweep_expired); cleanup_expired_role_grant_offers here wraps the latter with the required role_grant_offer_expire audit emission and is the piece most likely to be reused in a consumer's bespoke scheduler.

    Idempotency: the audit log has no tombstone on role_grant_offer_expire, so concurrent sweep runs double-audit. The expected deployment pattern is a single scheduled invocation per instance β€” matching query_session_cleanup_expired.

  • auth/credential_type_schema.ts

    Credential-type registry β€” how a request was authenticated.

    Three builtins: session (cookie-based), api_token (HTTP Bearer token), daemon_token (filesystem proof for the keeper account). Open-string registry on top so consumers can declare additional credential types (e.g. 'sso_assertion', 'agent_token') without an upstream release. RoleSpec.required_credential_types references entries from this registry; v1 keeps the field informative-only (consumed by auth/middleware.ts and the dispatcher). Mirrors the open-registry pattern used for RoleName, ScopeKindName, GrantPathName, and AuditEventTypeName.

    The Hono-side wire-validated CredentialType Zod enum (in hono_context.ts) is the closed-set narrow type middleware sets on the context; the constants below are the source of truth for those three string values. Future builtin credential types added here propagate to the wire enum by editing the import list.

  • auth/daemon_token_middleware.ts

    Daemon token rotation, persistence, and middleware.

    Manages the lifecycle of filesystem-resident daemon tokens: writing to disk, rotation on an interval, and HTTP middleware for authentication.

    Pure token primitives (schema, generation, validation) live in auth/daemon_token.ts. See docs/identity.md for design rationale.

  • auth/daemon_token.ts

    Daemon token primitives β€” schema, generation, and validation.

    Pure auth operations with no I/O or state management. The middleware, rotation, and persistence logic lives in auth/daemon_token_middleware.ts.

  • auth/deps.ts

    Stateless capabilities bundle for fuz_app backends.

    AppDeps is the central dependency injection type β€” injectable and swappable per environment (production vs test). Does not contain config (static values) or runtime state (mutable refs).

  • auth/grant_path_schema.ts

    Grant-path registry β€” the surfaces through which a role can be granted to an actor.

    Four builtins:

    - admin β€” granted by an admin via role_grant_offer_create (subject to the consumer's authorize callback) or admin-side direct grant. - self_service β€” toggled by the holder themselves via self_service_role_set (allowlisted by eligible_roles). - system β€” granted by system code paths (signup, automation, etc.) that don't fit either of the above. - bootstrap β€” granted exactly once during the bootstrap flow (keeper, admin on a fresh install).

    Open registry on top so consumers can declare additional paths (e.g. 'invite_only', 'sso_assertion') without an upstream release. RoleSpec.grant_paths references entries from this registry; the default for admin_actions.grantable_roles is grant_paths.includes('admin'), the default for self_service_role_actions eligibility is grant_paths.includes('self_service'). Mirrors the open-registry pattern used for RoleName, ScopeKindName, CredentialTypeName, and AuditEventTypeName.

  • auth/invite_queries.ts

    Invite database queries.

    CRUD operations for the invite table β€” creating invites, finding unclaimed matches, claiming, and cleanup.

  • auth/invite_schema.ts

    Invite types and client-safe schemas.

    Defines the runtime types for the invite system: invite creation, matching, and claiming.

  • auth/keyring.ts

    Key ring for cookie signing.

    Encapsulates secret keys and crypto operations. Keys are never exposed - only sign/verify operations are available. This prevents accidental logging or leakage of secrets.

    @example

    const keyring = create_keyring(process.env.SECRET_FUZ_COOKIE_KEYS); if (!keyring) throw new Error('No keys configured'); const signed = await keyring.sign('user:123:1700000000'); const result = await keyring.verify(signed); // result = { value: 'user:123:1700000000', key_index: 0 }
  • auth/middleware.ts

    Auth middleware stack factory.

    Creates the standard middleware layers (origin, session, request_context, bearer_auth, optional daemon_token) from configuration.

  • auth/migrations.ts

    Auth schema migrations.

    Ordered list of {name, up} migrations for the fuz identity system tables. Consumed by run_migrations with namespace 'fuz_auth'.

    Schema is not stabilized yet β€” append-only is NOT the rule. While fuz_app is pre-stable, migration bodies, names, and positions can change freely between versions; consumers upgrading across a schema change are expected to drop and re-bootstrap their dev/test databases (production deployments are not yet a supported use case). Once the schema is declared stable a hard append-only-after-publish rule will apply and the cliff will be called out in the release notes for that version. Until then: edit, rename, reorder, or replace migrations as needed; bias toward collapsing work into the existing v0/v1 entries rather than appending v2 patch migrations.

    To add a migration in the pre-stable phase, prefer extending an existing entry's body (consumers will re-bootstrap on upgrade). If you do append a new entry to auth_migrations, the runner will apply it on existing tracker rows β€” the same shape that will become mandatory once the schema stabilizes:

    // v2: add display_name to account { name: 'account_display_name', up: async (db) => { await db.query('ALTER TABLE account ADD COLUMN display_name TEXT'); }, },

    Migrations are forward-only (no down). Use IF NOT EXISTS / IF EXISTS for DDL safety. The name appears in error messages on failure.

  • auth/password_argon2.ts

    Argon2id password hashing implementation.

    Uses @node-rs/argon2 for native performance with OWASP-recommended parameters. Includes timing attack resistance via verify_dummy.

    Import argon2_password_deps for use as PasswordHashDeps in AppDeps.

  • auth/password.ts

    Password hashing type definitions.

    Defines the PasswordHashDeps injectable interface and PASSWORD_LENGTH_MIN. Concrete Argon2id implementation lives in auth/password_argon2.ts.

  • auth/request_context.ts

    Request context middleware and role_grant checking helpers.

    Two-phase identity resolution:

    1. Authentication (middleware) β€” create_request_context_middleware, bearer_auth, and daemon_token_middleware validate the credential (session cookie, bearer token, daemon token) and set c.var.account_id + c.var.credential_type on the Hono context. They do not resolve an acting actor or load role_grants; REQUEST_CONTEXT_KEY stays null at this stage, so account-grain identity is the only thing known. 2. Authorization (route-spec wrapper / RPC dispatcher) β€” after input validation, the per-route layer inspects the route. If the input schema declared acting?: ActingActor (reference equality with the canonical ActingActor schema) or the auth requires role_grants (role / keeper), apply_authorization_phase resolves the actor against c.var.account_id plus the validated acting value via resolve_acting_actor, builds the {account, actor, role_grants} context via build_request_context, and sets it on REQUEST_CONTEXT_KEY before auth guards fire. Authenticated routes that don't need an actor still get an account-only context via build_account_context so handler signatures stay uniform.

    Account-grain operations (logout, password_change, account_verify, etc.) declare neither acting nor role_grant-requiring auth, so no actor is resolved and their handlers see a RequestContext with actor: null + empty role_grants. They never trigger actor_required, which is what makes multi-actor logout work without first picking a persona.

    build_request_context loads account β†’ actor β†’ role_grants and verifies the actor.account_id === account.id binding. refresh_role_grants reloads role_grants on an existing context.

  • auth/role_grant_offer_action_specs.ts

    Role grant offer RPC action specs β€” declarative contract for the consentful-role-grants surface (offer lifecycle + admin revoke).

    Import this module for the specs, Input/Output schemas, ERROR_ROLE_GRANT_OFFER_* reason constants, and the all_role_grant_offer_action_specs registry. Handlers live in auth/role_grant_offer_actions.ts.

    Authorization enforcement: offer-lifecycle specs declare account+actor required (no roles) and rely on query_* IDOR guards or in-handler policy checks (e.g. role_grant_offer_list/_history elevate to admin only when inspecting another account β€” an input-dependent check that can't be expressed at the spec level). role_grant_revoke adds roles: ['admin'] β€” the RPC dispatcher's per-spec post-authorization auth gate (check_action_auth_post_authorization) rejects non-admin callers before the handler runs even though the endpoint hosts non-admin methods alongside.

  • auth/role_grant_offer_actions.ts

    Role grant offer RPC action handlers β€” the consentful-role-grants action surface.

    Seven actions: six offer-lifecycle methods (create / accept / decline / retract / list / history) plus role_grant_revoke (admin-only). All mount on a consumer's JSON-RPC endpoint via create_rpc_endpoint. The action specs themselves live in auth/role_grant_offer_action_specs.ts. Mutations declare side_effects: true so the RPC dispatcher wraps the handler in a DB transaction; role_grant_offer_list and role_grant_offer_history declare side_effects: false so they are addressable via GET.

    Authorization: - role_grant_offer_create β€” the grantor must hold an active role_grant for the role being offered, and that role's grant_paths must include 'admin'. Consumers needing a richer policy (e.g., "teacher may offer student in *their* classroom") pass an authorize callback that overrides the default. - role_grant_offer_accept / role_grant_offer_decline β€” keyed to the caller's account; query_* helpers enforce the IDOR guard. - role_grant_offer_retract β€” keyed to the caller's actor. - role_grant_offer_list / role_grant_offer_history β€” self by default; {account_id} is admin-only. - role_grant_revoke β€” spec-level auth: {role: 'admin'}; the RPC dispatcher rejects non-admin callers before the handler runs. The admin-grant-path gate prevents revoking keeper / daemon-scoped roles via this surface. Keys on actor_id to survive multi-actor accounts.

    Audit events are emitted in-transaction by the query layer (atomic with the role_grant write on accept/revoke) or by the handler via the bound deps.audit.emit_role_grant_target helper for single-event lifecycle transitions. audit.notify (SSE/WS broadcast) fires post-commit in both paths.

    WS notifications fan out post-commit via emit_after_commit when a notification_sender is wired: offer lifecycle transitions notify the counterparty, role_grant_revoke notifies the revokee plus each superseded pending offer's grantor.

  • auth/role_grant_offer_ddl.ts

    Role grant offer DDL β€” CREATE TABLE + index statements and the index-side sentinel constants the queries / migrations interpolate.

    Separated from auth/role_grant_offer_schema.ts so the schema module stays Zod-only (paired with auth/auth_ddl.ts and auth/audit_log_ddl.ts).

    An offer is a pending grant awaiting recipient consent. Lifecycle states are mutually exclusive via a CHECK constraint (role_grant_offer_single_terminal): at most one of accepted_at / declined_at / retracted_at may be set. On accept, the offer's resulting_role_grant_id links to the role_grant row produced by query_accept_offer.

  • auth/role_grant_offer_notifications.ts

    Role grant offer WebSocket notification specs, builders, and the narrow NotificationSender interface that decouples offer/revoke send sites from BackendWebsocketTransport.

    Six RemoteNotificationActionSpecs cover the consentful-role-grants lifecycle events the server pushes to affected accounts:

    - role_grant_offer_received β†’ recipient's sockets when an offer is created - role_grant_offer_retracted β†’ recipient's sockets when a grantor retracts - role_grant_offer_accepted β†’ grantor's sockets when the recipient accepts - role_grant_offer_declined β†’ grantor's sockets when the recipient declines - role_grant_offer_supersede β†’ grantor's sockets when a sibling accept, a revoke of the resulting role_grant, or destruction of the parent scope row obsoletes their pending offer - role_grant_revoke β†’ revokee's sockets when one of their active role_grants is revoked (companion to the role_grant_revoke audit event)

    Payloads are flat and normalized β€” RoleGrantOfferJson for the offer-lifecycle notifications (decline reason rides on offer.decline_reason, not a sibling field), and {role_grant_id, role, scope_id, reason?} for role_grant_revoke. The revokee/grantor/recipient account id travels via the send target (the NotificationSender.send_to_account argument), not in the payload.

    The specs surface as EventSpecs via create_action_event_spec β€” callers append role_grant_offer_notification_specs to their event_specs on create_app_server so the surface reflects them and DEV-mode broadcast validation catches payload drift.

  • auth/role_grant_offer_queries.ts

    Role grant offer database queries.

    Covers the offer side of the consentful-role-grants flow: create (with re-offer upsert), decline, retract, list, find-pending, sweep-expired, and the atomic query_accept_offer that bridges offer β†’ role_grant.

    IDOR guards are expressed in each helper's signature β€” decline/accept require the recipient's to_account_id, retract requires the grantor's from_actor_id.

  • auth/role_grant_offer_schema.ts

    Role grant offer types and client-safe schemas.

    An offer is a pending grant awaiting recipient consent. Lifecycle states are mutually exclusive via a CHECK constraint (role_grant_offer_single_terminal): at most one of accepted_at / declined_at / retracted_at may be set. On accept, the offer's resulting_role_grant_id links to the role_grant row produced by query_accept_offer.

    Table DDL and index-side sentinel constants live in auth/role_grant_offer_ddl.ts.

  • auth/role_grant_queries.ts

    Role grant database queries.

    Role grants are time-bounded, revocable grants of a role to an actor. The system is safe by default β€” no role_grant, no capability.

  • auth/role_schema.ts

    Role system β€” builtin roles, role specs, and extensible role schema factory.

    Defines the authorization policy vocabulary: which roles exist, their required credential types, the scope kinds each role applies to, and the grant paths through which each role can be granted. Each role gets a structured RoleSpec; the factory create_role_schema merges builtins with consumer-declared specs and validates every cross-axis field against the corresponding open registries (create_credential_type_schema, create_scope_kind_schema, create_grant_path_schema) at construction time so misconfigurations fire at server startup, not at first call.

    RoleSpec carries the four cross-axis fields that the dispatcher branches on: credential type, scope kind, grant path, and the role-name itself. v1 keeps the cross-axis fields informative-only (registry-membership validation, no INSERT-time enforcement); v2 may add (role, scope_kind) enforcement once the shape is clear from real consumer usage.

  • auth/scope_kind_schema.ts

    Scope-kind registry for role_grants and role_grant offers.

    Role grants have a polymorphic scope_id that references whatever entity the consumer chooses (a classroom, a tenant, a workspace, etc.); the scope_kind column tags each row with a machine-readable kind so admin UIs, codegen, and (in v2) registry-time (role, scope_kind) compatibility checks can read it without re-deriving from scope_id.

    scope_kind is encoded as nullable paired with the existing nullable scope_id β€” both null for global, both non-null for scoped, mismatch rejected at the DB layer by the role_grant_scope_kind_paired / role_grant_offer_scope_kind_paired CHECK constraints. There is no 'global' magic-string value; the global case is unambiguously (scope_kind=NULL, scope_id=NULL).

    Open registry, no builtins. Consumers declare their kinds via create_scope_kind_schema(consumer_kinds) and pass the result to create_role_schema so RoleSpec.applicable_scope_kinds can be validated at construction time. Mirrors the open-string registry pattern used for RoleName, AuditEventTypeName, and CredentialType.

    The literal 'GLOBAL' (uppercase) appears as an index expression inside the partial unique indexes on role_grant and role_grant_offer (COALESCE(scope_kind, 'GLOBAL')) β€” never as a column value, never as a registry entry. The uppercase form is structurally distinct from any consumer-declared kind (which match the lowercase ScopeKindName regex), so it cannot collide.

  • auth/self_service_role_action_specs.ts

    Unified self-service role toggle action spec β€” schemas, error reasons, and the codegen-ready registry.

    Client-safe: no query-layer or audit-write imports. Handler factory lives in auth/self_service_role_actions.ts.

  • auth/self_service_role_actions.ts

    Unified self-service role toggle RPC action.

    One static request_response action β€” self_service_role_set β€” that takes {role, enabled} and toggles a global role_grant on the caller for an allowlisted role. Idempotent in both directions: re-enabling an already-held role returns changed: false; disabling a role the caller doesn't hold returns changed: false.

    Eligibility is derived by default from RoleSpec.grant_paths β€” every role whose grant_paths includes 'self_service' (GRANT_PATH_SELF_SERVICE) is eligible. The factory accepts an optional eligible_roles override (validated against the supplied roles.role_specs at factory time so typos surface at startup instead of at first call) for deployments that want to lock the surface down further than the role spec declares. Roles outside the eligible set are rejected with forbidden + reason role_not_self_service_eligible.

    Audit metadata carries self_service: true so admin reviewers can distinguish self-toggled role_grants from admin grants/offers. The role_grant_create / role_grant_revoke metadata schemas declare self_service: z.boolean().optional() explicitly, so the field is part of the documented schema surface and is round-trip-validated by query_audit_log.

    Static method name β€” role lives in the input, not the method name β€” so the spec is codegen-compatible (satisfies RequestResponseActionSpec) and the surface stays constant as consumers add eligible roles. Mirrors the existing role_grant_offer_create({role}) precedent rather than generating per-role methods.

    Specs and schemas live in auth/self_service_role_action_specs.ts so client-side codegen can import the surface without dragging in the query layer.

  • auth/session_cookie.ts

    Generic session management for cookie-based auth.

    Parameterized on identity type via SessionOptions<TIdentity>. Handles signing, expiration, and key rotation. Apps provide encode/decode for their specific identity format.

    Cookie value format: ${encode(identity)}:${expires_at} (signed with HMAC-SHA256).

  • auth/session_middleware.ts

    Hono session boundary β€” cookie I/O, request-time middleware, and the session-creation helper shared by login / signup / bootstrap.

  • 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.

  • auth/signup_routes.ts

    Signup route spec for account creation.

    Public endpoint that creates an account. When open_signup is disabled (default), a matching unclaimed invite is required. When enabled, anyone can sign up without an invite. Follows the auth/bootstrap_routes.ts pattern.

  • auth/standard_action_specs.ts

    Aggregate spec list mirroring create_standard_rpc_actions on the backend.

    create_standard_rpc_actions (in auth/standard_rpc_actions.ts) bundles three action registries into one mounted RPC surface: admin + role_grant_offer + account. Frontends mounting that surface need the matching spec list to feed create_rpc_client so the typed Proxy knows about every standard method.

    Without this aggregate, every consumer spreads three (or four with self-service roles) all_*_action_specs imports at the typed-client site, the codegen-sources table, and any other registry construction β€” a triplicate that drifts silently on either side.

    Self-service role specs are not included β€” they're opt-in (require eligible_roles configuration) and not bundled into create_standard_rpc_actions. Consumers that mount them spread all_self_service_role_action_specs separately.

  • auth/standard_rpc_actions.ts

    Combined admin + role-grant-offer + account RPC actions for fuz_app consumers.

    The canonical "standard" RPC surface: every stock fuz_app RPC action a typical web consumer wants on one endpoint. Consumers that want a narrower surface drop down to the per-domain factories directly (create_admin_actions / create_role_grant_offer_actions / create_account_actions).

    Option routing: shared roles flows to both admin and role-grant-offer; app_settings goes to admin only; default_ttl_ms and authorize go to role-grant-offer only; max_tokens goes to account only; shared connection_closer flows to admin + account (role-grant-offer ignores); notification_sender reaches role-grant-offer transparently (admin + account ignore it).

    Paired with create_admin_rpc_adapters on the UI side.