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) => TOutput where ActionContext provides auth identity, DB, and framework context.

Declarations
#

6 declarations

view source

ActionContext
#

actions/action_rpc.ts view source

ActionContext

Per-request context provided to RPC action handlers.

Extends RouteContext with auth identity and logger. auth is RequestContext | null — handlers for authenticated actions can narrow via the auth middleware guarantee.

auth

The authenticated identity, or null for public routes.

type RequestContext | null

request_id

The JSON-RPC request ID from the envelope.

db

Transaction-scoped for mutations, pool-level for reads.

type Db

background_db

Always pool-level — for fire-and-forget effects that outlive the transaction.

type Db

pending_effects

Fire-and-forget side effects — push here for post-response flushing.

type Array<Promise<void>>

client_ip

Resolved client IP from the trusted-proxy middleware — 'unknown' if the middleware wasn't in the stack (e.g. WS dispatch) or couldn't resolve. Thread into audit_log_fire_and_forget as ip: ctx.client_ip for every user-initiated action so RPC audit rows match the REST convention. Pass null only for rows written outside a request (e.g. the permit_offer_expire cleanup sweep in auth/cleanup.ts).

type string

log

Logger instance.

type Logger

notify

Send a request-scoped JSON-RPC notification to the originator.

On streaming transports (WebSocket) this routes to the originating connection only. On the HTTP RPC transport this is a no-op with a DEV-mode warn — non-streaming transports have no channel for mid- request notifications. The streams field on an ActionSpec names the notification method this handler is expected to emit.

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

signal

AbortSignal that fires when the originating request is cancelled (client disconnect on HTTP, socket close on WebSocket). Streaming handlers should check this for early termination.

type AbortSignal

ActionHandler
#

actions/action_rpc.ts view source

ActionHandler<TInput, TOutput>

Handler function for an RPC action.

Receives validated input and an ActionContext with per-request deps. Returns the output value (serialized to JSON by the wrapper).

generics

TInput

default any

TOutput

default any

create_rpc_endpoint
#

actions/action_rpc.ts view source

(options: CreateRpcEndpointOptions): RouteSpec[]

Single JSON-RPC 2.0 endpoint — the canonical RPC transport binding.

Returns two RouteSpec entries (GET + POST on the same path) for apply_route_specs. The internal dispatcher handles:

1. Parse envelope — POST: JSON body as JsonrpcRequest. GET: method and params from query string. 2. Lookup method — find the RpcAction by method name. 3. Auth check — verify identity against the action's auth requirement. 4. Validate params — parse input against the action's input schema. 5. Dispatch — acquire DB handle (transaction for mutations, pool for reads), construct ActionContext, call handler, return JSON-RPC response.

GET is restricted to side_effects: false actions (cacheable reads). All errors use JSON-RPC format: {jsonrpc, id, error: {code, message, data?}}.

The RouteSpecs use auth: {type: 'none'} because auth is checked per-action inside the dispatcher, and transaction: false because transaction scope is per-action (mutations get a transaction, reads get pool).

options

endpoint path, actions, and logger

returns

RouteSpec[]

route specs (GET + POST) ready for apply_route_specs

throws

  • Error - if two actions share the same `spec.method` (registration-time

CreateRpcEndpointOptions
#

actions/action_rpc.ts view source

CreateRpcEndpointOptions

Options for create_rpc_endpoint.

path

Mount path for the endpoint (e.g., /api/rpc).

type string

actions

RPC actions to serve.

type Array<RpcAction>

log

Logger instance for handler context.

type Logger

action_ip_rate_limiter

Per-IP rate limiter consulted for actions whose spec declares rate_limit: 'ip' or 'both'. null disables the IP check. Per-action gate via action.spec.rate_limit. Same limiter is shared with the WebSocket action dispatcher — one budget per action, not per transport.

type RateLimiter | null

action_account_rate_limiter

Per-actor rate limiter consulted for actions whose spec declares rate_limit: 'account' or 'both'. Keyed on request_context.actor.id. null disables the account check. Same limiter is shared with the WebSocket action dispatcher.

type RateLimiter | null

rpc_action
#

actions/action_rpc.ts view source

<TSpec extends RequestResponseActionSpec>(spec: TSpec, handler: ActionHandler<output<TSpec["input"]>, output<TSpec["output"]>>): RpcAction

Pair a spec with a handler while preserving per-method input/output types.

Constructing {spec, handler} literals widens handler to ActionHandler<any, any>, so spec/handler drift (renamed Zod schema, output field removal, input shape change) slips past the typechecker. rpc_action(spec, handler) binds the handler signature to (input: z.infer<spec.input>, ctx) => z.infer<spec.output> via the generic spec parameter — drift surfaces at the call site.

Fits fuz_app's factory-closure pattern (handlers close over grantable_roles, app_settings ref, notification_sender, etc.). zzz uses a different shape — a codegen-keyed Record<Method, Handler> map typed via generated ActionInputs/ActionOutputs — which works when handlers are pure (no closure state) and specs are codegen-enumerated. fuz_app's admin + permit-offer actions have neither, so per-pair typing at the registration site is the right fit.

spec

type TSpec

handler

type ActionHandler<output<TSpec["input"]>, output<TSpec["output"]>>

returns

RpcAction

RpcAction
#

Depends on
#

Imported by
#