actions
30 modules
actions/action_bridge.ts
Bridge functions to derive RouteSpec and EventSpec from ActionSpec.
Action specs define the contract (method, input/output, auth, side effects). Bridge functions produce transport-specific specs from them. HTTP-specific concerns (path, handler) come from options.
actions/action_codegen.ts
actions/action_event_data.ts
Action event data types โ discriminated union for state machine data.
39-variant discriminated union across 3 action kinds (request_response, remote_notification, local_call). Each variant narrows the data shape by kind, phase, and step.
actions/action_event_helpers.ts
Action event helper functions โ type guards, validators, and data creation.
actions/action_event_types.ts
Action event type definitions โ state machine constants and environment interface.
actions/action_event.ts
ActionEvent โ state machine for action lifecycle management.
Manages an action through its phases (send_request โ receive_response, etc.) and steps (initial โ parsed โ handling โ handled/failed).
actions/action_peer.ts
ActionPeer โ symmetric send/receive for JSON-RPC actions.
Wraps a Transports registry and ActionEventEnvironment to provide bidirectional action dispatch via JSON-RPC 2.0.
actions/action_registry.ts
ActionRegistry โ query and filter utility over
ActionSpecUnion[].Vocabulary (see the
docs/directory): -*_handled_*โ request_response specs the named side receives (so the named side owns the handler). Used by codegen to emit typed handler maps. -*_relevant_to_*โ the loose "everything this side might encounter" set, used by the typed-Proxy method enums (FrontendActionMethod,BackendActionMethod). -broadcast_*โ kind-narrowremote_notificationset with thestreams-target exclusion. Today this matches what the broadcast API exposes. -backend_initiated_*โ forward-looking kind-agnostic version of the broadcast set. Same content today; will diverge when local_calls or backendrequest_responsejoin the backend's typed surface.Cache discipline:
spec_by_method(Map) and the internal streams-target set lazy-memoize because the Map is consulted per-RPC dispatch (actions/frontend_rpc_client.ts wires it intolookup_action_spec) and the streams set is rebuilt by two public getters. Array-returning getters recompute on each call so callers can mutate the result freely (.sort(),.push(injected)on a copy, etc.) without affecting the registry โ codegen is a build-time path where the extra.filter/.mapwork is negligible.actions/action_rpc.ts
Single JSON-RPC 2.0 endpoint from action specs.
create_rpc_endpoint produces
RouteSpec[](GET + POST on one path) with an internal dispatcher. Method name lives in the JSON-RPC envelope (POST body or GET query string), not the URL. Auth is checked per-action inside the dispatcher.Handler signature:
(input: TInput, ctx: ActionContext) => TOutputwhere ActionContext provides auth identity, DB, and framework context.actions/action_spec.ts
Action spec types โ the canonical source of truth for action contracts.
Extracted from zzz's action system. Action specs define method, kind, auth, side effects, and input/output schemas. Bridge functions in actions/action_bridge.ts derive RouteSpec and EventSpec from them.
@see actions/action_rpc.ts for the JSON-RPC dispatcher @see actions/register_action_ws.ts for the WebSocket dispatcher
actions/action_types.ts
Shared type surface for the action system โ Action (the composable
{spec, handler?}tuple) and re-exports of the canonical ActionContext + ActionHandler shapes.Sits above actions/action_spec.ts (pure Zod schemas) and below the dispatchers (actions/register_action_ws.ts, actions/action_rpc.ts, actions/perform_action.ts). Extracted so the shared protocol actions (e.g. heartbeat_action) can name them without pulling in server-only modules.
HTTP RPC and WebSocket dispatchers both call into perform_action, and both pass the same ActionContext to the handler. Consumers inject domain deps via factory closures the same way HTTP RPC factories do (see auth/standard_rpc_actions.ts).
actions/broadcast_api.ts
Backend-initiated broadcast notification plumbing โ generic across consumers.
Builds a typed
{method_name: (input) => Promise<void>}object from a list of action specs. Each call validates input against the spec, wraps it in a JSON-RPC notification, and either broadcasts to every connection or fans out with a per-connection ACL predicate.Counterpart to register_action_ws: that handles request-scoped dispatch (frontend-initiated), this handles broadcast (backend-initiated). Together they cover the two primitives fuz_app consumers share. Request-scoped streaming (
completion_progress,zap_applyevents) stays onctx.notifyinside a handler โ it's socket-scoped, not broadcast.Extracted from zzz's
backend_actions_api.tsto stop the pattern from drifting across consumers.actions/cancel.ts
Shared cancel action โ a fuz_app protocol action validating the spec+handler tuple pattern on a notification-kind action.
Semantics: the client sends `{jsonrpc, method: 'cancel', params: {request_id}}` to abort an in-flight request on the same socket. register_action_ws intercepts this notification and aborts the matching pending request's
ctx.signal. Unknown ids are no-ops by design โ races between response arrival and cancel delivery are safe without extra coordination.The handler field is an empty stub: cancel semantics are dispatcher-owned (the dispatcher has the
{request_id โ AbortController}map, not the handler). The handler exists for symmetry with other protocol actions like heartbeat_action; the dispatcher never calls it. Consumers spread cancel_action (or the protocol_actions bundle from actions/protocol.ts) into their server'sactionsarray sospec_by_methodknows about it (enabling input validation on incoming cancels) and so create_rpc_client codegen producesapp.api.cancel()when desired โ thoughFrontendWebsocketClient.request({signal})sends the cancel on abort without needing the typed API.Wire format is snake_case
cancelwith{request_id}, not MCP's$/cancelRequestwith{requestId}โ fuz_app's WS transport isn't MCP, and adopting MCP's convention would leak protocol-specific framing into the base transport. If an MCP adapter is ever built, the translation layer at the adapter is the right seam.actions/compile_action_registry.ts
Shared registration loop for action-dispatcher endpoints.
create_rpc_endpoint (HTTP RPC) and register_action_ws (WebSocket) both build a
Map<method, RpcAction>from a list of action specs and gate the build on the same registry-time invariants:1. Auth-shape biconditional โ per assert_route_auth_acting_biconditional in http/auth_shape.ts: `auth.actor !== 'none' โบ input declares acting?: ActingActor`. Fires for every spec with non-null auth. 2. Rate-limit / account axis โ
rate_limit: 'account' | 'both'requiresauth.account === 'required'; without an account on the request there is no key for the per-account bucket. 3. JSON-RPC ยง4.2 wire validity โrequest_responsespecs whose handler will reach the dispatch map must not usez.null()for input (the wire format forbids"params": null; usez.void()for parameterless methods). 4. Duplicate method names โ JSON-RPC keys onmethod, so every spec in the array must declare a uniquemethodregardless of kind / handler presence.Pre-consolidation each dispatcher inlined these checks; the comment in
register_action_ws.tsliterally said "mirrors the HTTP RPC registration check" but nothing kept them mirrored. Centralizing the loop closes the most likely future drift surface.actions/connection_closer.ts
Narrow structural capability for closing live WebSocket connections tied to a session token hash, API token id, or account id.
Why this exists. Per-message authorization phase on WebSocket (actions/perform_action.ts) reloads role_grants from the DB on every message but does NOT re-query session / token validity โ that trade-off keeps chatty connections fast. The cost: revocation doesn't actually disconnect open sockets unless something closes them.
transports_ws_auth_guard.tsis the listener-based seam (audit-event โ close), but it only fires after the audit INSERT succeeds โ if the INSERT fails (DB error, pool exhausted, handler dies mid-flight) the listener never runs and the live socket keeps working with a stale RequestContext until disconnect.Used by self-service revocation handlers (
account_session_revoke/_revoke_all,account_token_revoke,logout,password) and the admin revoke-all handlers (admin_session_revoke_all,admin_token_revoke_all) to eagerly drop affected sockets BEFORE emitting the corresponding audit event. The audit listener stays as a fail-safe for out-of-band emit sites (admin tools, scheduled jobs, SSE-driven flows).close_sockets_for_*is idempotent so the second pass is a no-op.Mirrors
zzz_server'sclose_sockets_for_*calls inaccount.rs::logout_inner/_password_inner/handlers/account.rs::handle_account_session_revoke[_all]/_token_revoke(landed 2026-05-16).BackendWebsocketTransport satisfies this interface structurally, so consumers pass their transport instance directly (same shape as NotificationSender). The interface stays local so handlers don't couple to the concrete transport, and tests can inject a capturing stub with no WS machinery.
actions/frontend_rpc_client.ts
Frontend-only typed RPC client factory.
Bundles the `ActionRegistry + ActionEventEnvironment + Transports + ActionPeer + create_rpc_client + create_throwing_api` boilerplate every consumer repeats.
lookup_action_handlerdefaults to() => undefined(HTTP-only frontends rarely need handlers); passoptions.lookup_action_handlerto wire WS-pushedremote_notificationdispatch or areceive_error/local_callhook.Returns both Proxy shapes from one factory call:
-
apiโ typed throwing Proxy.await api.foo(input)returns the unwrapped value or throws anErrorcarrying{code, data}from the JSON-RPC error. Use at hot-path call sites. -api_resultโ typed Result-shaped Proxy.await api_result.foo(input)returnsResult<{value}, {error: JsonrpcErrorObject}>. Use when call sites want to inspecterror.data.reasonwithout try/catch โ and anywhere allocating anErrorper{ok: false}is wasteful (e.g. reconnect-stormservice_unavailablepaths). Result is the protocol primitive; the throwing form is a wrapper over it. Both share the same underlying transport โ pick per call site, no construction cost.Generic
TApiis the consumer's typed Proxy interface. The `as unknown as TApi` double cast happens inside the helper so call sites get a typed return value without the cast hostility.api's type isThrowingApi<TApi>โ the mapped type strips the Result wrapper.const {api, api_result} = create_frontend_rpc_client<MyActionsApi>({ specs: all_specs, }); // hot path: await api.account_verify() // rare branch: const r = await api_result.account_verify(); if (!r.ok) { โฆ }Returns the underlying
peerandenvironmentalongside the two api shapes so advanced consumers (zzz-style frontends needing extra transports / WS notification handlers / action-history wiring) can extend without recreating the bundle.local_callspecs inspecsno-op unlesslookup_action_handlerresolves a handler for the'execute'phase. Frontend-sidelocal_callis uncommon; the factory targets wire-dispatched actions by default.actions/heartbeat.ts
Shared heartbeat action โ a fuz_app protocol action carrying both a spec and a handler in one tuple. Consumers spread heartbeat_action (or the protocol_actions bundle from actions/protocol.ts) into the server's
actionsarray so disconnect detection works identically across every repo without per-consumer ping plumbing.The client's activity-aware heartbeat timer (in FrontendWebsocketClient) issues a
heartbeatrequest whenever the connection has been idle for its configured interval; server-side the dispatcher tracks receive time, so incoming heartbeats keep the socket alive without any handler-level state.Nullary input/output today.
{client_ts, server_ts}fields can be added later if clock-skew telemetry ever matters โ the Action container is open for additions without churning consumer call sites.actions/perform_action.ts
Transport-agnostic dispatch core shared by HTTP RPC and WebSocket action dispatchers.
perform_action runs the post-parse pipeline that every action-spec handler must traverse:
1. Pre-validation auth (401) โ short-circuits unauthenticated callers on
'required'axes before input validation runs, so callers never seeinvalid_paramsfor methods with required input. 2. Validate params (400) โspec.input.safeParse(raw_params)withz.void()/?? {}rules. The validated input lands inside the function so the authorization phase readsactingas a typed Zod field. 3. Authorization phase โ whenauth.actor !== 'none'(orauth.account !== 'none' && actor === 'none'), resolves the actor via apply_authorization_phase against the suppliedaccount_idplusvalidated_input.acting. Failures fold into a JSON-RPC error envelope. The test-harness escape hatch lives in the caller โ passpreset.request_contextto skip the live phase and use a pre-baked context instead. 4. Post-authorization auth (403) โ gatesauth.credential_typesandauth.rolesagainst the resolved context. 5. Rate limit (429) โ per-action IP / account throttling, throttle- requests semantics (every invocation records, regardless of outcome). 6. Dispatch + DEV-only output validation + error normalization โspec.side_effectspicks transaction (deps.db.transaction) vs pool. Handler throws roll back the transaction; the catch sits outside the transaction boundary. Handler outputs are validated againstspec.outputunder DEV (logs an error on mismatch, never throws, never mutates the result).The function is pure data โ it never touches a Hono context, so HTTP RPC, REST bridge (when on the action surface), and WS dispatch all call into it the same way and bind the discriminated PerformActionResult to their wire shape.
actions/protocol.ts
Canonical bundles of fuz_app's protocol actions โ
heartbeatandcancel. Spread these into consumer registrations on both sides of the wire so the registries stay symmetric without per-consumer plumbing.Protocol actions are wire-protocol concerns (liveness, abort) shipped by fuz_app, not consumer domain logic. The split is intentional: the server needs
{spec, handler}tuples to drive dispatch; the frontend ActionRegistry only stores specs. The codegeninclude_protocol_actions: falsedefault (in actions/action_codegen.ts) is the third leg of this contract โ protocol actions are excluded from generated typed surfaces because consumers spread them in at registration time.Adding a future protocol action (e.g. clock-skew probe, reconnect-resume token) means appending to these arrays in one place; no consumer migration required.
actions/register_action_ws.ts
WebSocket JSON-RPC dispatch โ the low-level WS transport binding.
Most consumers should mount WS endpoints via register_ws_endpoint (actions/register_ws_endpoint.ts), which wraps this function with the standard upgrade stack (origin check + auth + optional role). This module stays exported as the lower-level entry point for tests that drive the dispatcher directly via create_ws_test_harness.
Symmetric to create_rpc_endpoint (from actions/action_rpc.ts): both transports parse their wire envelope, then call the shared perform_action core (actions/perform_action.ts) for the post-parse pipeline. WS-specific concerns โ connection lifecycle, heartbeat, cancel-notification interception, socket-scoped notify โ stay in this module; everything else (auth gates, input validation, authorization phase, rate limiting, transactional dispatch, DEV output validation, thrown-error normalization) is shared.
Auth expectations
The consumer is responsible for rejecting unauthenticated upgrades *before* routing to this handler (fuz_app's require_auth middleware, or register_ws_endpoint which wires it for you). Per-action auth runs inside perform_action on every message via the same gates HTTP RPC uses.
actions/register_ws_endpoint.ts
Composed WebSocket endpoint registration โ the idiomatic consumer entry point for mounting a fuz_app WS endpoint.
Wraps the standard upgrade stack every consumer writes by hand:
1.
verify_request_source(allowed_origins)โ reject disallowed origins before the upgrade handshake runs. 2. require_auth โ reject unauthenticated upgrades. 3. Authorization phase โ resolve the acting actor against the authenticated account plus an optional?acting=<uuid>query string, and build the RequestContext that per-message dispatch reads. Multi-actor accounts must supply?actingto pick a persona; single-actor accounts work without it. 4. Optionalrequire_role(required_roles)โ for endpoints gated to a non-empty any-of set of roles.Then delegates to register_action_ws for per-message JSON-RPC dispatch.
actions/request_tracker.svelte.ts
Request tracker โ manages pending JSON-RPC requests with timeouts.
Uses SvelteMap for reactive pending request tracking.
actions/rpc_client.ts
Typed RPC client โ creates a Proxy-based API from action specs.
Two tiers of usage: - Tier 1 (simple, for tx): transport send/receive, Result return. No
environment. - Tier 2 (full, for zzz): ActionEvent lifecycle withenvironment.Pass the consumer's generated
ActionsApiinterface as<TApi>to flow full type safety through without an explicit cast at the call site.actions/socket.svelte.ts
Frontend WebSocket client โ portable, Svelte-reactive, implements WebsocketConnection.
Plain class with
$staterunes (no Cell inheritance, no app coupling). Drop into any SvelteKit frontend as the underlying connection for FrontendWebsocketTransport. Handles auto-reconnect with exponential backoff, respects WS_CLOSE_SESSION_REVOKED (no reconnect loop after the server revokes auth), exposes reactive status for UI indicators, and ships three correctness primitives default-on:-
FrontendWebsocketClient.requestโ promise-based JSON-RPC with auto-assigned ids and a pending-id map. Intercepts responses on the message path so request/response correlation is transport-level rather than re-invented per consumer. - Durable queue โrequest()calls made while disconnected buffer up to DEFAULT_QUEUE_MAX_SIZE requests and flush on reopen. Overflow rejects withqueue_overflow. RawFrontendWebsocketClient.sendis drop-on-disconnect (fire-and-forget notifications want that). - Activity-aware heartbeat โ idles fire a sharedheartbeatrequest; receive-silence past DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT closes with WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT and lets auto-reconnect pick back up.actions/transports_http.ts
HTTP transport โ sends JSON-RPC messages via HTTP POST (or GET for reads).
actions/transports_ws_auth_guard.ts
WebSocket auth guard โ bridges audit events to BackendWebsocketTransport.
Why this exists. register_action_ws captures
account_idandcredential_typeat upgrade time and reuses them for every message. perform_action's per-message authorization phase reloads role_grants from the DB, but session and token VALIDITY are not re-queried โ that trade-off keeps chatty WS connections fast. The cost: nothing in the dispatch path notices when a session is revoked or a token is rotated. This guard is the enforcement mechanism โ it listens on the audit chain and closes affected sockets when revocation events fire, so revocation actually takes effect on existing connections. Without it,session_revoke/token_revokeare no-ops for open WS connections.Mirror of realtime/sse_auth_guard.ts for the backend WebSocket transport. Dispatches audit events to the right
close_sockets_for_*method so consumers do not re-implement the switch themselves.For standard WS endpoints mounted via
AppServerOptions.ws_endpoints, create_app_server composes the guard automatically perWsEndpointSpec.auth_guard. For custom wiring, append the handler inside the consumer'saudit_factorybody (or viaaudit.on_event_chain.push(...)post-assembly).actions/transports_ws_backend.ts
Backend WebSocket transport โ manages server-side WebSocket connections with session tracking and revocation support.
actions/transports_ws.ts
Frontend WebSocket transport โ thin adapter over WebsocketRpcConnection.
Delegates request/response correlation, the durable queue, the heartbeat, and
AbortSignal-driven cancel to the underlying connection (the canonical implementation is FrontendWebsocketClient). The transport's own job is the Transport contract: route inbound server-pushed messages intopeer.receiveand translate the connection'sPromise<R>/ThrownJsonrpcError shape into JsonrpcResponseOrError envelopes. No parallel pending-request map.actions/transports.ts
Transport abstraction for action communication.
Provides the Transport interface and Transports registry for managing multiple transports with fallback behavior.
actions/ws_endpoint_spec.ts
WsEndpointSpec โ the canonical WebSocket endpoint declaration consumed by create_app_server's
ws_endpointsoption (mirror of RpcEndpointSpec for HTTP RPC).Lives in its own module so both server/app_server.ts (which mounts endpoints from these specs) and http/surface.ts (which threads the resolved spec list into surface generation) can import it without a cycle between the two.