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 see invalid_params for methods with required input. 2. Validate params (400)spec.input.safeParse(raw_params) with z.void() / ?? {} rules. The validated input lands inside the function so the authorization phase reads acting as a typed Zod field. 3. Authorization phase — when auth.actor !== 'none' (or auth.account !== 'none' && actor === 'none'), resolves the actor via apply_authorization_phase against the supplied account_id plus validated_input.acting. Failures fold into a JSON-RPC error envelope. The test-harness escape hatch lives in the caller — pass preset.request_context to skip the live phase and use a pre-baked context instead. 4. Post-authorization auth (403) — gates auth.credential_types and auth.roles against 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 normalizationspec.side_effects picks transaction (deps.db.transaction) vs pool. Handler throws roll back the transaction; the catch sits outside the transaction boundary. Handler outputs are validated against spec.output under 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.

Declarations
#

5 declarations

view source

perform_action
#

actions/perform_action.ts view source

(input: PerformActionInput, deps: PerformActionDeps): Promise<PerformActionResult>

The shared dispatch core. Pure data — no Hono context, no socket. Each transport calls into this with pre-parsed inputs and binds the result to its wire shape.

Phase order: 401 → 400 → 403 → handler. On the test-preset path the dispatcher skips the live authorization phase and uses the supplied pre-baked context for post-authorization checks; pre-validation 401 still fires when the harness omits account_id.

input

deps

returns

Promise<PerformActionResult>

perform_action_result_to_envelope
#

actions/perform_action.ts view source

(id: string | number, result: PerformActionResult): { jsonrpc: string; id: string | number; } & ({ result: unknown; } | { error: { [x: string]: unknown; code: -32700 | -32600 | -32601 | -32602 | -32603 | (number & $brand<...>); message: string; data?: unknown; }; })

Build a JSON-RPC response envelope from a PerformActionResult for transports that wire over the JSON-RPC 2.0 message shape (HTTP RPC + WS).

id

type string | number

result

returns

{ jsonrpc: string; id: string | number; } & ({ result: unknown; } | { error: { [x: string]: unknown; code: -32700 | -32600 | -32601 | -32602 | -32603 | (number & $brand<"JsonrpcServerErrorCode">); message: string; data?: unknown; }; })

PerformActionDeps
#

actions/perform_action.ts view source

PerformActionDeps

Per-deps inputs to perform_action. Each transport supplies its own pool-level Db and rate limiters; the dispatcher wraps in a transaction iff spec.side_effects is true.

Pool-resilient fire-and-forget effects (audit writes) run through AppDeps.audit.emit from the action factory's closure — the dispatcher never sees the audit emitter. The bound emitter owns the pool.

db

Pool-level DB. The dispatcher wraps in db.transaction for side_effects: true actions.

type Db

pending_effects

Eager fire-and-forget pool-write queue, flushed by the transport's try/finally via flush_pending_effects.

type Array<Promise<void>>

post_commit_effects

Deferred post-commit thunks pushed via emit_after_commit, flushed by the transport's try/finally after the handler returns.

type Array<() => void | Promise<void>>

log

Logger threaded into ActionContext.log.

type Logger

action_ip_rate_limiter

Per-IP limiter (shared across transports). null disables.

type RateLimiter | null

action_account_rate_limiter

Per-account limiter (shared across transports). null disables.

type RateLimiter | null

PerformActionInput
#

actions/perform_action.ts view source

PerformActionInput

Per-call inputs to perform_action. Each transport assembles this from its wire envelope + connection identity.

action

The resolved spec + handler (transport does method lookup).

raw_params

Raw params from the wire envelope (post-JsonrpcRequest.parse, pre-spec.input.safeParse).

type unknown

request_id

JSON-RPC request id — echoed onto the response.

account_id

Authenticated account id, or null for anonymous.

type string | null

credential_type

Credential type the request arrived on, or null for anonymous.

type CredentialType | null

client_ip

Resolved client IP ('unknown' if upstream couldn't resolve).

type string

signal

Per-request abort signal. HTTP: c.req.raw.signal. WS: AbortSignal.any([socket, request]).

type AbortSignal

notify

Send a request-scoped notification. HTTP: DEV-warn-and-drop. WS: socket-scoped.

type (method: string, params: unknown) => void

connection_id

Stable per-socket id on WS; undefined on HTTP.

type Uuid

preset

Test-harness escape hatch. When set, the live authorization phase is skipped and request_context is used directly for post-authorization checks + handler dispatch. Production callers leave this undefined.

type {request_context: RequestContext | null}

PerformActionResult
#

actions/perform_action.ts view source

PerformActionResult

Discriminated result of perform_action. Each transport binds this to its wire shape: HTTP RPC folds the error into a JSON-RPC envelope and returns via c.json; WS sends the response over the socket.

Depends on
#

Imported by
#