/**
 * 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.
 *
 * @module
 */
import { z } from 'zod';
import type { Logger } from '@fuzdev/fuz_util/log.js';
import type { Uuid } from '@fuzdev/fuz_util/id.js';
import type { RequestResponseActionSpec } from './action_spec.js';
import { type RouteSpec } from '../http/route_spec.js';
import { type RequestActorContext, type RequestContext } from '../auth/request_context.js';
import { type CredentialType } from '../hono_context.js';
import type { Db } from '../db/db.js';
import { type JsonrpcRequestId } from '../http/jsonrpc.js';
import type { RateLimiter } from '../rate_limiter.js';
/**
 * Per-request context provided to action handlers across every transport
 * (HTTP RPC, WebSocket, REST bridge). Built once per dispatched action by
 * `perform_action` and threaded into the handler.
 *
 * `auth` is `RequestContext | null` — handlers for authenticated actions
 * can narrow via the dispatcher's authorization-phase guarantee.
 *
 * Single handler context shape across every transport. Consumers inject
 * domain deps via factory closures the same way HTTP RPC factories do.
 */
export interface ActionContext {
    /** The authenticated identity, or `null` for public routes. */
    auth: RequestContext | null;
    /** The JSON-RPC request ID from the envelope. */
    request_id: JsonrpcRequestId;
    /**
     * Stable per-socket connection id on WebSocket transport; `undefined`
     * on HTTP RPC. Consumers key per-connection domain state on this
     * directly; HTTP handlers ignore it.
     */
    connection_id?: Uuid;
    /**
     * Transaction-scoped when `spec.side_effects` is true (the dispatcher
     * wraps in `db.transaction`); pool-level otherwise. Handlers that
     * need rollback-resilient writes call `deps.audit.emit(ctx, input)`,
     * which captures the pool inside its closure.
     */
    db: Db;
    /**
     * Eager fire-and-forget queue — push the in-flight `Promise<void>` for
     * pool writes already running (audit emits, session touch, api-token
     * usage tracking). Drained via `flush_pending_effects` after the
     * handler returns.
     */
    pending_effects: Array<Promise<void>>;
    /**
     * Deferred post-commit thunks — do not push directly; reach for
     * `emit_after_commit(ctx, fn)` from `pending_effects.ts`. The flush
     * site invokes each thunk after the handler (and any wrapping
     * `db.transaction`) returns.
     */
    post_commit_effects: Array<() => void | Promise<void>>;
    /**
     * 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 `deps.audit.emit` 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
     * `role_grant_offer_expire` cleanup sweep in `auth/cleanup.ts`).
     */
    client_ip: string;
    /**
     * Credential channel the request arrived on (`'session'` | `'api_token'` |
     * `'daemon_token'`), or `null` for anonymous requests. Same value the
     * dispatcher's `credential_types` gate consumed at step 4 — exposed here
     * so handlers can record it in audit metadata (defense in depth: the
     * gate may be loosened or bypassed in a future refactor, but the audit
     * row preserves what actually authenticated the request).
     */
    credential_type: CredentialType | null;
    /** Logger instance. */
    log: Logger;
    /**
     * 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.
     */
    notify: (method: string, params: unknown) => void;
    /**
     * AbortSignal that fires when the originating request is cancelled
     * (client disconnect on HTTP, socket close or per-request cancel
     * notification on WebSocket). Streaming handlers should check this
     * for early termination.
     */
    signal: AbortSignal;
}
/**
 * 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).
 */
export type ActionHandler<TInput = any, TOutput = any> = (input: TInput, ctx: ActionContext) => TOutput | Promise<TOutput>;
/**
 * `ActionContext` narrowed to a non-null `RequestContext`.
 *
 * Used by handlers whose spec declares `auth.account === 'required'`
 * (with `auth.actor === 'none'`) — the dispatcher's pre-validation 401
 * gate guarantees `request_context` is populated before the handler
 * runs, but the actor slot stays null because no `acting` resolution
 * happened. Selected automatically by `rpc_action`'s conditional return
 * type for the account-grain tier.
 */
export interface ActionAuthContext extends Omit<ActionContext, 'auth'> {
    auth: RequestContext;
}
/**
 * Handler signature for an account-grain RPC action — `auth.account === 'required'`
 * and `auth.actor === 'none'`. Mirrors `ActionHandler` but tightens the
 * `ctx.auth` slot to the non-null `RequestContext` (with `actor: null`).
 */
export type AuthActionHandler<TInput = any, TOutput = any> = (input: TInput, ctx: ActionAuthContext) => TOutput | Promise<TOutput>;
/**
 * `ActionContext` narrowed to a resolved acting actor.
 *
 * Used by handlers whose spec declares `auth.actor === 'required'` —
 * the dispatcher's authorization phase resolves an actor (per
 * registry-time invariant 2 the input declares `acting?: ActingActor`),
 * so `ctx.auth.actor` is non-null. Selected automatically by
 * `rpc_action`'s conditional return type for the actor-implying tier.
 */
export interface ActionActorContext extends Omit<ActionContext, 'auth'> {
    auth: RequestActorContext;
}
/**
 * Handler signature for an actor-implying RPC action — `auth.actor === 'required'`.
 * Mirrors `ActionHandler` but tightens the `ctx.auth` slot to the
 * non-null `RequestActorContext` (with non-null `actor`).
 */
export type ActorActionHandler<TInput = any, TOutput = any> = (input: TInput, ctx: ActionActorContext) => TOutput | Promise<TOutput>;
/**
 * An RPC action — combines an action spec with its handler.
 *
 * The spec defines the contract (method, auth, schemas, side effects).
 * The handler implements the behavior.
 */
export interface RpcAction {
    spec: RequestResponseActionSpec;
    handler: ActionHandler;
}
/**
 * Conditional handler shape for `rpc_action` — picks the narrowest
 * `ctx.auth` type the dispatcher's runtime guarantee allows:
 *
 * - `auth.actor === 'required'` → `ActorActionHandler` (`ctx.auth: RequestActorContext`).
 * - `auth.account === 'required' && auth.actor === 'none'` → `AuthActionHandler` (`ctx.auth: RequestContext`).
 * - else (public, optional axes) → `ActionHandler` (`ctx.auth: RequestContext | null`).
 *
 * The bracketed form `[T] extends ['required']` defeats distributive
 * conditionals so a degraded `AuthAxisState` union (when the spec was
 * typed without preserving its literal) falls through to the loosest
 * tier instead of collapsing to the narrowest.
 */
export type HandlerForSpec<TSpec extends RequestResponseActionSpec> = [
    TSpec['auth']['actor']
] extends ['required'] ? ActorActionHandler<z.infer<TSpec['input']>, z.infer<TSpec['output']>> : [TSpec['auth']['account']] extends ['required'] ? AuthActionHandler<z.infer<TSpec['input']>, z.infer<TSpec['output']>> : ActionHandler<z.infer<TSpec['input']>, z.infer<TSpec['output']>>;
/**
 * Pair a spec with a handler while preserving per-method input/output types
 * and selecting the narrowest `ctx.auth` shape the spec literal admits.
 *
 * 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.
 *
 * The `ctx.auth` narrowing follows the spec's `auth.account` /
 * `auth.actor` literals (see `HandlerForSpec`): an actor-implying spec
 * gets `ctx.auth: RequestActorContext`; an account-grain spec gets
 * `ctx.auth: RequestContext`; everything else stays `ctx.auth:
 * RequestContext | null`. Handlers can rely on the dispatcher's
 * runtime guarantee without a manual narrowing call.
 *
 * 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 + role-grant-offer actions have neither, so per-pair typing
 * at the registration site is the right fit.
 *
 * Spec-literal preservation is load-bearing: declare specs with
 * `satisfies RequestResponseActionSpec` (canonical) so `auth.actor`
 * keeps its `'required'` / `'none'` literal type. A spec typed
 * directly as `RequestResponseActionSpec` widens the axes to
 * `AuthAxisState` and the handler defaults to the loosest tier — sound,
 * but loses the ergonomic narrowing.
 *
 * @example
 * ```ts
 * // actor-implying spec → ctx.auth: RequestActorContext
 * rpc_action(role_grant_revoke_action_spec, async (input, ctx) => {
 *   const revoker_id = ctx.auth.actor.id; // no narrowing needed
 * });
 *
 * // account-grain spec → ctx.auth: RequestContext (actor: null)
 * rpc_action(account_verify_action_spec, (_input, ctx) => {
 *   return to_session_account(ctx.auth.account); // no narrowing needed
 * });
 * ```
 */
export declare const rpc_action: <TSpec extends RequestResponseActionSpec>(spec: TSpec, handler: HandlerForSpec<TSpec>) => RpcAction;
/** Options for `create_rpc_endpoint`. */
export interface CreateRpcEndpointOptions {
    /** Mount path for the endpoint (e.g., `/api/rpc`). */
    path: string;
    /** RPC actions to serve. */
    actions: Array<RpcAction>;
    /** Logger instance for handler context. */
    log: Logger;
    /**
     * 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.
     */
    action_ip_rate_limiter?: RateLimiter | null;
    /**
     * Per-account rate limiter consulted for actions whose spec declares
     * `rate_limit: 'account'` or `'both'`. Keyed on
     * `request_context.account.id` (account-grain — billed to the
     * authenticated account regardless of which actor was resolved).
     * `null` disables the account check. Same limiter is shared with the
     * WebSocket action dispatcher.
     */
    action_account_rate_limiter?: RateLimiter | null;
}
/**
 * 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. **Pre-validation auth** — short-circuit `unauthenticated` when no
 *    account is on the request, before input validation runs.
 * 4. **Authorization phase** — resolve the acting actor (when the action's
 *    auth requires role_grants or its input declares `acting?: ActingActor`)
 *    and build the request context. Runs before input validation so
 *    role-grant-grain auth checks return 403 before 400 invalid_params;
 *    `acting` is read from raw params via a string typeguard.
 * 5. **Post-authorization auth** — enforce role / keeper requirements
 *    against the request context.
 * 6. **Validate params** — parse input against the action's `input` schema.
 * 7. **Rate limit** — per-action IP / account throttling.
 * 8. **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).
 *
 * @param options - endpoint path, actions, and logger
 * @returns route specs (GET + POST) ready for `apply_route_specs`
 * @throws Error if two actions share the same `spec.method` (registration-time
 *   duplicate detection); also throws if any action's `spec.input` is
 *   `z.null()` (JSON-RPC 2.0 §4.2 forbids `params: null` on the wire — use
 *   `z.void()` for parameterless methods).
 */
export declare const create_rpc_endpoint: (options: CreateRpcEndpointOptions) => Array<RouteSpec>;
//# sourceMappingURL=action_rpc.d.ts.map