@fuzdev/fuz_app 0.53.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/actions/CLAUDE.md +68 -13
  2. package/dist/actions/action_codegen.d.ts +13 -0
  3. package/dist/actions/action_codegen.d.ts.map +1 -1
  4. package/dist/actions/action_codegen.js +15 -1
  5. package/dist/actions/action_rpc.d.ts +60 -7
  6. package/dist/actions/action_rpc.d.ts.map +1 -1
  7. package/dist/actions/action_rpc.js +158 -44
  8. package/dist/actions/register_action_ws.d.ts +4 -4
  9. package/dist/actions/register_action_ws.js +6 -6
  10. package/dist/actions/register_ws_endpoint.d.ts +20 -7
  11. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  12. package/dist/actions/register_ws_endpoint.js +30 -5
  13. package/dist/actions/transports.d.ts.map +1 -1
  14. package/dist/actions/transports.js +0 -4
  15. package/dist/auth/CLAUDE.md +230 -63
  16. package/dist/auth/account_actions.d.ts +6 -6
  17. package/dist/auth/account_actions.d.ts.map +1 -1
  18. package/dist/auth/account_actions.js +8 -11
  19. package/dist/auth/account_queries.d.ts +6 -3
  20. package/dist/auth/account_queries.d.ts.map +1 -1
  21. package/dist/auth/account_queries.js +14 -5
  22. package/dist/auth/account_routes.d.ts +7 -10
  23. package/dist/auth/account_routes.d.ts.map +1 -1
  24. package/dist/auth/account_routes.js +70 -23
  25. package/dist/auth/account_schema.d.ts +19 -0
  26. package/dist/auth/account_schema.d.ts.map +1 -1
  27. package/dist/auth/account_schema.js +20 -0
  28. package/dist/auth/admin_action_specs.d.ts +45 -11
  29. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  30. package/dist/auth/admin_action_specs.js +23 -8
  31. package/dist/auth/admin_actions.d.ts +8 -7
  32. package/dist/auth/admin_actions.d.ts.map +1 -1
  33. package/dist/auth/admin_actions.js +11 -18
  34. package/dist/auth/audit_log_queries.d.ts +53 -14
  35. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  36. package/dist/auth/audit_log_queries.js +45 -2
  37. package/dist/auth/audit_log_schema.d.ts +55 -1
  38. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  39. package/dist/auth/audit_log_schema.js +19 -3
  40. package/dist/auth/bearer_auth.d.ts +9 -7
  41. package/dist/auth/bearer_auth.d.ts.map +1 -1
  42. package/dist/auth/bearer_auth.js +13 -21
  43. package/dist/auth/cleanup.d.ts.map +1 -1
  44. package/dist/auth/cleanup.js +5 -0
  45. package/dist/auth/daemon_token_middleware.d.ts +23 -11
  46. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  47. package/dist/auth/daemon_token_middleware.js +26 -20
  48. package/dist/auth/deps.d.ts +14 -0
  49. package/dist/auth/deps.d.ts.map +1 -1
  50. package/dist/auth/middleware.d.ts.map +1 -1
  51. package/dist/auth/middleware.js +4 -2
  52. package/dist/auth/migrations.d.ts +15 -7
  53. package/dist/auth/migrations.d.ts.map +1 -1
  54. package/dist/auth/migrations.js +15 -7
  55. package/dist/auth/permit_offer_action_specs.d.ts +45 -6
  56. package/dist/auth/permit_offer_action_specs.d.ts.map +1 -1
  57. package/dist/auth/permit_offer_action_specs.js +38 -7
  58. package/dist/auth/permit_offer_actions.d.ts +2 -2
  59. package/dist/auth/permit_offer_actions.d.ts.map +1 -1
  60. package/dist/auth/permit_offer_actions.js +106 -95
  61. package/dist/auth/permit_offer_notifications.d.ts +10 -0
  62. package/dist/auth/permit_offer_notifications.d.ts.map +1 -1
  63. package/dist/auth/permit_offer_queries.d.ts +68 -9
  64. package/dist/auth/permit_offer_queries.d.ts.map +1 -1
  65. package/dist/auth/permit_offer_queries.js +147 -35
  66. package/dist/auth/permit_offer_schema.d.ts +23 -1
  67. package/dist/auth/permit_offer_schema.d.ts.map +1 -1
  68. package/dist/auth/permit_offer_schema.js +5 -0
  69. package/dist/auth/permit_queries.d.ts +17 -5
  70. package/dist/auth/permit_queries.d.ts.map +1 -1
  71. package/dist/auth/permit_queries.js +19 -8
  72. package/dist/auth/request_context.d.ts +360 -32
  73. package/dist/auth/request_context.d.ts.map +1 -1
  74. package/dist/auth/request_context.js +442 -60
  75. package/dist/auth/route_guards.d.ts +10 -4
  76. package/dist/auth/route_guards.d.ts.map +1 -1
  77. package/dist/auth/route_guards.js +14 -8
  78. package/dist/auth/self_service_role_action_specs.d.ts +2 -0
  79. package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
  80. package/dist/auth/self_service_role_action_specs.js +2 -0
  81. package/dist/auth/self_service_role_actions.d.ts +6 -5
  82. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  83. package/dist/auth/self_service_role_actions.js +32 -19
  84. package/dist/db/migrate.d.ts +11 -7
  85. package/dist/db/migrate.d.ts.map +1 -1
  86. package/dist/db/migrate.js +9 -6
  87. package/dist/dev/setup.d.ts.map +1 -1
  88. package/dist/dev/setup.js +5 -3
  89. package/dist/hono_context.d.ts +77 -0
  90. package/dist/hono_context.d.ts.map +1 -1
  91. package/dist/hono_context.js +50 -0
  92. package/dist/http/CLAUDE.md +80 -17
  93. package/dist/http/error_schemas.d.ts +92 -1
  94. package/dist/http/error_schemas.d.ts.map +1 -1
  95. package/dist/http/error_schemas.js +73 -16
  96. package/dist/http/jsonrpc_errors.d.ts +27 -2
  97. package/dist/http/jsonrpc_errors.d.ts.map +1 -1
  98. package/dist/http/jsonrpc_errors.js +26 -2
  99. package/dist/http/route_spec.d.ts +62 -4
  100. package/dist/http/route_spec.d.ts.map +1 -1
  101. package/dist/http/route_spec.js +117 -21
  102. package/dist/http/schema_helpers.d.ts +13 -1
  103. package/dist/http/schema_helpers.d.ts.map +1 -1
  104. package/dist/http/schema_helpers.js +21 -2
  105. package/dist/http/surface.d.ts +10 -1
  106. package/dist/http/surface.d.ts.map +1 -1
  107. package/dist/http/surface.js +2 -2
  108. package/dist/server/app_server.d.ts.map +1 -1
  109. package/dist/server/app_server.js +11 -1
  110. package/dist/testing/CLAUDE.md +23 -17
  111. package/dist/testing/admin_integration.d.ts.map +1 -1
  112. package/dist/testing/admin_integration.js +15 -13
  113. package/dist/testing/adversarial_headers.js +1 -1
  114. package/dist/testing/app_server.js +2 -2
  115. package/dist/testing/audit_completeness.d.ts.map +1 -1
  116. package/dist/testing/audit_completeness.js +21 -7
  117. package/dist/testing/auth_apps.d.ts.map +1 -1
  118. package/dist/testing/auth_apps.js +6 -3
  119. package/dist/testing/entities.d.ts +2 -1
  120. package/dist/testing/entities.d.ts.map +1 -1
  121. package/dist/testing/entities.js +1 -0
  122. package/dist/testing/integration_helpers.d.ts +4 -2
  123. package/dist/testing/integration_helpers.d.ts.map +1 -1
  124. package/dist/testing/integration_helpers.js +9 -5
  125. package/dist/testing/middleware.d.ts +12 -8
  126. package/dist/testing/middleware.d.ts.map +1 -1
  127. package/dist/testing/middleware.js +67 -25
  128. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  129. package/dist/testing/rpc_helpers.js +3 -1
  130. package/dist/testing/schema_generators.d.ts.map +1 -1
  131. package/dist/testing/schema_generators.js +12 -0
  132. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  133. package/dist/testing/ws_round_trip.js +5 -1
  134. package/dist/ui/CLAUDE.md +16 -10
  135. package/dist/ui/PermitOfferForm.svelte +14 -0
  136. package/dist/ui/PermitOfferForm.svelte.d.ts +6 -0
  137. package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -1
  138. package/dist/ui/admin_accounts_state.svelte.d.ts +8 -1
  139. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  140. package/dist/ui/admin_accounts_state.svelte.js +14 -3
  141. package/dist/ui/permit_offers_state.svelte.d.ts +9 -1
  142. package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -1
  143. package/dist/ui/permit_offers_state.svelte.js +7 -1
  144. package/package.json +1 -1
@@ -65,8 +65,10 @@ the dispatcher's per-action rate-limit hook. Same hook fires on the HTTP
65
65
  RPC dispatcher (`create_rpc_endpoint`) and the WebSocket dispatcher
66
66
  (`register_action_ws`) — one budget per action, not per transport.
67
67
  `'ip'` keys on the resolved client IP; `'account'` keys on
68
- `request_context.actor.id` (post-auth) and is rejected at registration
69
- when paired with `auth: 'public'` (no actor to key on); `'both'` runs
68
+ `request_context.account.id` (post-auth, account-grain every
69
+ authenticated action has an account regardless of whether an actor was
70
+ resolved) and is rejected at registration when paired with
71
+ `auth: 'public'` (no account to key on); `'both'` runs
70
72
  both checks. **Throttle-requests semantics** — every invocation records,
71
73
  regardless of outcome (different from REST login's throttle-failures
72
74
  that resets on success). The motivating threat is admin mutation oracles
@@ -233,15 +235,20 @@ route specs on the same path (GET + POST) that share one internal
233
235
  dispatcher. Per-action auth lives inside the dispatcher; the outer routes
234
236
  use `auth: {type: 'none'}` and `transaction: false`.
235
237
 
236
- Dispatcher phase order (POST; GET differs only at step 1):
238
+ Dispatcher phase order (POST; GET differs only at step 1). Mirrors the
239
+ REST authorization order in `http/route_spec.ts` so HTTP RPC and REST
240
+ fail with the same priority (401 → 403 → 400 → handler):
237
241
 
238
242
  1. **Parse envelope** — POST body as `JsonrpcRequest` (parse errors → JSON-RPC `parse_error` 400). GET reads `method`, `id`, `params` from query string; missing `method`/`id` → 400 `invalid_request`. Integer `id` normalization: `?id=42` matches `{id: 42}`.
239
243
  2. **Lookup method** — `Map<method, RpcAction>`. Unknown method → `method_not_found`. Duplicate methods throw at construction.
240
244
  3. **GET read restriction** — GET is rejected for `side_effects: true` actions (`invalid_request` with "must use POST").
241
- 4. **Auth check** — via `check_action_auth(spec.auth, request_context, credential_type)`. `keeper` requires `credential_type === 'daemon_token'` AND `has_role(request_context, 'keeper')` the `has_role` alone is insufficient, session/bearer cannot elevate. `{role}` uses `has_role`. Failure `unauthenticated` / `forbidden`.
242
- 5. **Validate params** — `spec.input.safeParse(raw_params ?? null-if-null-schema)`. Failure → `invalid_params` with `{issues}`.
243
- 6. **Dispatch** — `spec.side_effects` picks transaction (`route.db.transaction(tx => execute(tx))`) vs pool (`route.db`). Handler throws roll back the transaction the catch sits outside the transaction boundary.
244
- 7. **DEV-only output validation** — `spec.output.safeParse(output)` runs only under `DEV` (from `esm-env`). On mismatch: `log.error(...)`, return response unchanged; never throws, never mutates status.
245
+ 4. **Pre-validation auth** — `check_action_auth_pre_validation(spec.auth, account_id)`. Short-circuits with `unauthenticated` (-32001 / 401) when `auth !== 'public'` and no `ACCOUNT_ID_KEY` is on the request. Fires before input validation so unauthenticated callers don't leak `invalid_params` for methods with required input. Public actions skip the rest of the auth path.
246
+ 5. **Authorization phase** — for non-public actions, when `is_actor_implying_auth(spec.auth)` (`'keeper'` or `{role}`) or `input_schema_declares_acting(spec.input)` (the input has the canonical `acting?: ActingActor` field), `apply_authorization_phase` resolves the actor against `c.var.account_id` plus the raw `acting` string read off `params` (no schema validation yet), builds the `{account, actor, permits}` `RequestContext`, and sets `REQUEST_CONTEXT_KEY`. Authenticated-but-actor-less routes still build an account-only context via `build_account_context`. Resolution failures come back as `AuthorizationFailure` (`{status, body}`) — the auth domain stops short of producing a `Response` so each transport binds it. The RPC dispatcher folds the failure into a JSON-RPC envelope: `error.code` from `http_status_to_jsonrpc_error_code(failure.status)` (400 → `invalid_params` for `actor_required` / `actor_not_on_account`, 500 → `internal_error` for `no_actors_on_account` and `account_vanished`), `error.message` from the reason string, and `error.data: {reason, ...rest}` flattens any diagnostic fields (e.g. `available[]` for `actor_required`). The two 500 reasons are kept distinct: `no_actors_on_account` names a signup invariant violation (the actor enumeration succeeded and came back empty); `account_vanished` names a torn-read race (the account or actor row was deleted between credential validation and the dispatcher's follow-up `build_request_context` / `build_account_context` step). REST emits the same `body` directly via `c.json(body, status)` so its surface stays consistent with other middleware-emitted plain bodies. See `../auth/CLAUDE.md` § Middleware and the root `../../../CLAUDE.md` § Cleanest architecture takes priority for the rationale.
247
+ 6. **Post-authorization auth** — `check_action_auth_post_authorization(spec.auth, request_context, credential_type)`. `keeper` requires `credential_type === 'daemon_token'` AND `has_role(request_context, 'keeper')` the `has_role` alone is insufficient, session/bearer cannot elevate; failure attaches `{reason: ERROR_KEEPER_REQUIRES_DAEMON_TOKEN, credential_type}` under `error.data`. `{role}` uses `has_role`; failure attaches `{reason: ERROR_INSUFFICIENT_PERMISSIONS, required_role}`. Both surface as `forbidden` (-32002 / 403). `'authenticated'` already cleared step 4.
248
+ 7. **Validate params** — `spec.input.safeParse(params)` where `params` is `raw_params` for `z.void()` schemas, otherwise `raw_params ?? {}` (HTTP convention: empty body = empty object). Registration rejects `z.null()` inputs because JSON-RPC 2.0 §4.2 forbids `params: null`. Failure → `invalid_params` with `{issues}`.
249
+ 8. **Rate limit** — `spec.rate_limit` (`'ip' | 'account' | 'both'`); shared limiter pair with the WS dispatcher. Throttle-requests semantics — every invocation records, regardless of outcome. Account-keyed limiting bills `request_context.account.id` (every authenticated action has one). Failure → `rate_limited` (-32006 / 429) with `{retry_after}`.
250
+ 9. **Dispatch** — `spec.side_effects` picks transaction (`route.db.transaction(tx => execute(tx))`) vs pool (`route.db`). Handler throws roll back the transaction — the catch sits outside the transaction boundary.
251
+ 10. **DEV-only output validation** — `spec.output.safeParse(output)` runs only under `DEV` (from `esm-env`). On mismatch: `log.error(...)`, return response unchanged; never throws, never mutates status.
245
252
 
246
253
  Error paths: `ThrownJsonrpcError` (duck-typed via `err instanceof Error &&
247
254
  typeof err.code === 'number'`) preserves code + data verbatim, status via
@@ -284,9 +291,9 @@ Use this at every spec → handler binding site so handler-type errors
284
291
  surface at the factory call instead of at runtime:
285
292
 
286
293
  ```ts
287
- export const create_admin_actions = (deps, options) => [
288
- rpc_action(admin_account_list_action_spec, account_list_handler),
289
- rpc_action(admin_session_revoke_all_action_spec, session_revoke_all_handler),
294
+ export const create_account_actions = (deps, options) => [
295
+ rpc_action(account_verify_action_spec, verify_handler),
296
+ rpc_action(account_session_list_action_spec, session_list_handler),
290
297
  // …
291
298
  ];
292
299
  ```
@@ -297,9 +304,57 @@ handlers close over factory-captured deps (`log`, `on_audit_event`,
297
304
  `options.app_settings`, `options.max_tokens`), so per-pair typing via
298
305
  `rpc_action()` is the right shape here: the binding happens at
299
306
  construction time and the handler keeps its closure. Applied across
300
- `admin_actions.ts` + `permit_offer_actions.ts` + `account_actions.ts`
301
- each pairs a spec imported from its `*_action_specs.ts` sibling with
302
- a closure-bound handler.
307
+ `account_actions.ts` for the account-grain self-service surface (auth:
308
+ `'authenticated'`, no `acting` in input the dispatcher does not
309
+ resolve an actor); the actor-implying registries (`admin_actions.ts`,
310
+ `permit_offer_actions.ts`, `self_service_role_actions.ts`) use the
311
+ `rpc_actor_action` variant below.
312
+
313
+ ### `rpc_actor_action(spec, handler)` — actor-narrowed variant
314
+
315
+ Sibling factory for handlers whose dispatcher always resolves an acting
316
+ actor — actions with `auth: 'keeper' | {role}` or input that declares
317
+ `acting?: ActingActor`. The dispatcher's authorization phase populates
318
+ `ctx.auth` with a non-null `RequestActorContext` before any of these
319
+ handlers runs, so `rpc_actor_action`'s handler signature types
320
+ `ctx: ActionActorContext` (with `auth: RequestActorContext`) and the
321
+ handler body skips the `require_request_actor(ctx.auth)` narrowing
322
+ call:
323
+
324
+ ```ts
325
+ rpc_actor_action(permit_revoke_action_spec, async (input, ctx) => {
326
+ // ctx.auth is RequestActorContext — no narrowing needed.
327
+ const revoker_id = ctx.auth.actor.id;
328
+ // …
329
+ });
330
+ ```
331
+
332
+ The runtime binding is identical to `rpc_action` — both register the
333
+ same `RpcAction` shape on the action map. The change is compile-time
334
+ only: forgetting the actor narrowing on an actor-implying action used
335
+ to require either an `auth.actor!` non-null assertion or a
336
+ `require_request_actor` call; `rpc_actor_action` lets the type
337
+ reflect what the dispatcher already guarantees, which closes the bug
338
+ class where the narrowing call is missed and the handler is left
339
+ operating against a possibly-null actor.
340
+
341
+ Applied uniformly across the actor-implying registries: every handler
342
+ in `admin_actions.ts` (all eleven specs declare `auth: {role: 'admin'}`
343
+
344
+ - `acting: ActingActor` on input, so the dispatcher always resolves an
345
+ actor — list-style handlers that don't read `ctx.auth.actor` still bind
346
+ through `rpc_actor_action` for type-uniformity), every handler in
347
+ `permit_offer_actions.ts` (every spec there declares
348
+ `acting: ActingActor`), and the single `self_service_role_set` handler
349
+ in `self_service_role_actions.ts`. The rule is "actor-implying spec →
350
+ `rpc_actor_action`" regardless of whether the handler body reads
351
+ `ctx.auth.actor` — the dispatcher's runtime guarantee is what the type
352
+ should reflect, and uniform binding keeps a future handler that does
353
+ need the actor from accidentally landing on the looser binder.
354
+ Account-grain handlers in `account_actions.ts` keep `rpc_action`:
355
+ their auth is `'authenticated'`, their inputs don't declare `acting`,
356
+ so the dispatcher genuinely runs in `needs_actor: false` mode and
357
+ `ctx.auth.actor` is null.
303
358
 
304
359
  ## Transports (`transports.ts`, `transports_http.ts`, `transports_ws.ts`, `transports_ws_backend.ts`)
305
360
 
@@ -154,6 +154,19 @@ export declare const to_action_spec_output_identifier: (method: string) => strin
154
154
  * follows so wrappers no longer pre-register imports a per-spec emit might
155
155
  * not actually use.
156
156
  *
157
+ * **Optional-input detection.** The emitted parameter is `input?:` (caller
158
+ * may omit the argument) when either (a) the schema accepts `undefined` —
159
+ * `z.optional(z.strictObject(...))` and similar wrappers — or (b) the
160
+ * schema accepts the empty object `{}` — `z.strictObject({acting:
161
+ ActingActor})` and other all-optional-fields strict objects. The second
162
+ * probe mirrors the dispatcher's HTTP convention (`raw_params ?? {}` for
163
+ * non-`z.void()` schemas in `actions/action_rpc.ts` / `http/route_spec.ts`):
164
+ * if a request with no params reaches the handler, this is the value the
165
+ * schema is asked to validate. A schema with required fields fails both
166
+ * probes and stays `input:` (required at the typed surface). Refinements
167
+ * and transforms run as part of `safeParse`, so their accept/reject
168
+ * decisions feed into the optional/required choice naturally.
169
+ *
157
170
  * @param options.sync_returns_value - When true (default), sync `local_call`
158
171
  * methods return the output value directly; when false they're wrapped in
159
172
  * `Result<{value, error}>` like async methods. Set to `false` if your
@@ -1 +1 @@
1
- {"version":3,"file":"action_codegen.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_codegen.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"action_codegen.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_codegen.ts"],"names":[],"mappings":"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"}
@@ -330,6 +330,19 @@ export const to_action_spec_output_identifier = (method) => `${to_action_spec_id
330
330
  * follows so wrappers no longer pre-register imports a per-spec emit might
331
331
  * not actually use.
332
332
  *
333
+ * **Optional-input detection.** The emitted parameter is `input?:` (caller
334
+ * may omit the argument) when either (a) the schema accepts `undefined` —
335
+ * `z.optional(z.strictObject(...))` and similar wrappers — or (b) the
336
+ * schema accepts the empty object `{}` — `z.strictObject({acting:
337
+ ActingActor})` and other all-optional-fields strict objects. The second
338
+ * probe mirrors the dispatcher's HTTP convention (`raw_params ?? {}` for
339
+ * non-`z.void()` schemas in `actions/action_rpc.ts` / `http/route_spec.ts`):
340
+ * if a request with no params reaches the handler, this is the value the
341
+ * schema is asked to validate. A schema with required fields fails both
342
+ * probes and stays `input:` (required at the typed surface). Refinements
343
+ * and transforms run as part of `safeParse`, so their accept/reject
344
+ * decisions feed into the optional/required choice naturally.
345
+ *
333
346
  * @param options.sync_returns_value - When true (default), sync `local_call`
334
347
  * methods return the output value directly; when false they're wrapped in
335
348
  * `Result<{value, error}>` like async methods. Set to `false` if your
@@ -343,8 +356,9 @@ export const generate_actions_api_method_signature = (spec, imports, options) =>
343
356
  const collections_path = options?.collections_path ?? DEFAULT_COLLECTIONS_PATH;
344
357
  const innermost_type_name = zod_get_base_type(spec.input);
345
358
  const has_input = innermost_type_name !== 'null' && innermost_type_name !== 'void';
359
+ const input_optional = has_input && (spec.input.safeParse(undefined).success || spec.input.safeParse({}).success);
346
360
  const input_param = has_input
347
- ? `input${spec.input.safeParse(undefined).success ? '?' : ''}: ActionInputs['${spec.method}']`
361
+ ? `input${input_optional ? '?' : ''}: ActionInputs['${spec.method}']`
348
362
  : 'input?: void';
349
363
  if (has_input)
350
364
  imports.add_type(collections_path, 'ActionInputs');
@@ -15,7 +15,7 @@ import { z } from 'zod';
15
15
  import type { Logger } from '@fuzdev/fuz_util/log.js';
16
16
  import type { RequestResponseActionSpec } from './action_spec.js';
17
17
  import { type RouteSpec } from '../http/route_spec.js';
18
- import { type RequestContext } from '../auth/request_context.js';
18
+ import { type RequestActorContext, type RequestContext } from '../auth/request_context.js';
19
19
  import type { Db } from '../db/db.js';
20
20
  import { type JsonrpcRequestId } from '../http/jsonrpc.js';
21
21
  import type { RateLimiter } from '../rate_limiter.js';
@@ -72,6 +72,24 @@ export interface ActionContext {
72
72
  * Returns the output value (serialized to JSON by the wrapper).
73
73
  */
74
74
  export type ActionHandler<TInput = any, TOutput = any> = (input: TInput, ctx: ActionContext) => TOutput | Promise<TOutput>;
75
+ /**
76
+ * `ActionContext` narrowed to a resolved acting actor.
77
+ *
78
+ * Returned to handlers bound via `rpc_actor_action` — the dispatcher's
79
+ * authorization phase has already run for actor-implying auth or
80
+ * `acting`-declaring inputs, so `ctx.auth.actor` is non-null and the
81
+ * handler skips the `require_request_actor(ctx.auth)` narrowing call.
82
+ */
83
+ export interface ActionActorContext extends Omit<ActionContext, 'auth'> {
84
+ auth: RequestActorContext;
85
+ }
86
+ /**
87
+ * Handler function for an RPC action whose dispatcher always resolves an
88
+ * acting actor (`auth: 'keeper' | {role}` or input declaring
89
+ * `acting?: ActingActor`). Mirrors `ActionHandler` but tightens the
90
+ * `ctx.auth` slot to the non-null `RequestActorContext`.
91
+ */
92
+ export type ActorActionHandler<TInput = any, TOutput = any> = (input: TInput, ctx: ActionActorContext) => TOutput | Promise<TOutput>;
75
93
  /**
76
94
  * An RPC action — combines an action spec with its handler.
77
95
  *
@@ -101,6 +119,30 @@ export interface RpcAction {
101
119
  * at the registration site is the right fit.
102
120
  */
103
121
  export declare const rpc_action: <TSpec extends RequestResponseActionSpec>(spec: TSpec, handler: ActionHandler<z.infer<TSpec["input"]>, z.infer<TSpec["output"]>>) => RpcAction;
122
+ /**
123
+ * Variant of `rpc_action` for handlers whose spec always resolves an
124
+ * acting actor — actions with `auth: 'keeper' | {role}` or inputs that
125
+ * declare `acting?: ActingActor`. The dispatcher's authorization phase
126
+ * runs before the handler, populates `ctx.auth` with a non-null
127
+ * `RequestActorContext`, and `rpc_actor_action` reflects that
128
+ * guarantee in the handler signature so the handler body skips the
129
+ * `require_request_actor(ctx.auth)` narrowing call (and the bug class
130
+ * where forgetting that call fails open against a `null` actor).
131
+ *
132
+ * The runtime binding is identical to `rpc_action` — both register the
133
+ * same `RpcAction` shape on the action map. Only the compile-time
134
+ * handler signature differs.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * rpc_actor_action(permit_revoke_action_spec, async (input, ctx) => {
139
+ * // ctx.auth is RequestActorContext — no require_request_actor() needed.
140
+ * const revoker_id = ctx.auth.actor.id;
141
+ * // ...
142
+ * });
143
+ * ```
144
+ */
145
+ export declare const rpc_actor_action: <TSpec extends RequestResponseActionSpec>(spec: TSpec, handler: ActorActionHandler<z.infer<TSpec["input"]>, z.infer<TSpec["output"]>>) => RpcAction;
104
146
  /** Options for `create_rpc_endpoint`. */
105
147
  export interface CreateRpcEndpointOptions {
106
148
  /** Mount path for the endpoint (e.g., `/api/rpc`). */
@@ -118,10 +160,12 @@ export interface CreateRpcEndpointOptions {
118
160
  */
119
161
  action_ip_rate_limiter?: RateLimiter | null;
120
162
  /**
121
- * Per-actor rate limiter consulted for actions whose spec declares
163
+ * Per-account rate limiter consulted for actions whose spec declares
122
164
  * `rate_limit: 'account'` or `'both'`. Keyed on
123
- * `request_context.actor.id`. `null` disables the account check.
124
- * Same limiter is shared with the WebSocket action dispatcher.
165
+ * `request_context.account.id` (account-grain billed to the
166
+ * authenticated account regardless of which actor was resolved).
167
+ * `null` disables the account check. Same limiter is shared with the
168
+ * WebSocket action dispatcher.
125
169
  */
126
170
  action_account_rate_limiter?: RateLimiter | null;
127
171
  }
@@ -134,9 +178,18 @@ export interface CreateRpcEndpointOptions {
134
178
  * 1. **Parse envelope** — POST: JSON body as `JsonrpcRequest`. GET: `method`
135
179
  * and `params` from query string.
136
180
  * 2. **Lookup method** — find the `RpcAction` by method name.
137
- * 3. **Auth check** — verify identity against the action's `auth` requirement.
138
- * 4. **Validate params** parse input against the action's `input` schema.
139
- * 5. **Dispatch** — acquire DB handle (transaction for mutations, pool for reads),
181
+ * 3. **Pre-validation auth** — short-circuit `unauthenticated` when no
182
+ * account is on the request, before input validation runs.
183
+ * 4. **Authorization phase** — resolve the acting actor (when the action's
184
+ * auth requires permits or its input declares `acting?: ActingActor`)
185
+ * and build the request context. Runs before input validation so
186
+ * permit-grain auth checks return 403 before 400 invalid_params;
187
+ * `acting` is read from raw params via a string typeguard.
188
+ * 5. **Post-authorization auth** — enforce role / keeper requirements
189
+ * against the request context.
190
+ * 6. **Validate params** — parse input against the action's `input` schema.
191
+ * 7. **Rate limit** — per-action IP / account throttling.
192
+ * 8. **Dispatch** — acquire DB handle (transaction for mutations, pool for reads),
140
193
  * construct `ActionContext`, call handler, return JSON-RPC response.
141
194
  *
142
195
  * GET is restricted to `side_effects: false` actions (cacheable reads).
@@ -1 +1 @@
1
- {"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAE9F,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC,OAAO,EAGN,KAAK,gBAAgB,EAGrB,MAAM,oBAAoB,CAAC;AAU5B,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,+DAA+D;IAC/D,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC5B,iDAAiD;IACjD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,8DAA8D;IAC9D,EAAE,EAAE,EAAE,CAAC;IACP,oFAAoF;IACpF,aAAa,EAAE,EAAE,CAAC;IAClB,2EAA2E;IAC3E,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC;;;;;;;OAOG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;OAQG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,CACxD,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,aAAa,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,SAAS,yBAAyB,EACjE,MAAM,KAAK,EACX,SAAS,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KACvE,SAGD,CAAC;AAEH,yCAAyC;AACzC,MAAM,WAAW,wBAAwB;IACxC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC1B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5C;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACjD;AAkDD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,wBAAwB,KAAG,KAAK,CAAC,SAAS,CAkTtF,CAAC"}
1
+ {"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"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"}
@@ -15,11 +15,11 @@ import { z } from 'zod';
15
15
  import { DEV } from 'esm-env';
16
16
  import {} from '../http/route_spec.js';
17
17
  import { get_client_ip } from '../http/proxy.js';
18
- import { get_request_context, has_role } from '../auth/request_context.js';
19
- import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
18
+ import { apply_authorization_phase, get_request_context, has_role, input_schema_declares_acting, is_actor_implying_auth, } from '../auth/request_context.js';
19
+ import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
20
20
  import { is_null_schema, is_void_schema } from '../http/schema_helpers.js';
21
21
  import { JSONRPC_VERSION, JsonrpcRequest, } from '../http/jsonrpc.js';
22
- import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, JSONRPC_ERROR_CODES, } from '../http/jsonrpc_errors.js';
22
+ import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, http_status_to_jsonrpc_error_code, JSONRPC_ERROR_CODES, } from '../http/jsonrpc_errors.js';
23
23
  import { ERROR_INSUFFICIENT_PERMISSIONS, ERROR_KEEPER_REQUIRES_DAEMON_TOKEN, } from '../http/error_schemas.js';
24
24
  /**
25
25
  * Pair a spec with a handler while preserving per-method input/output types.
@@ -43,22 +43,68 @@ export const rpc_action = (spec, handler) => ({
43
43
  spec,
44
44
  handler: handler,
45
45
  });
46
+ /**
47
+ * Variant of `rpc_action` for handlers whose spec always resolves an
48
+ * acting actor — actions with `auth: 'keeper' | {role}` or inputs that
49
+ * declare `acting?: ActingActor`. The dispatcher's authorization phase
50
+ * runs before the handler, populates `ctx.auth` with a non-null
51
+ * `RequestActorContext`, and `rpc_actor_action` reflects that
52
+ * guarantee in the handler signature so the handler body skips the
53
+ * `require_request_actor(ctx.auth)` narrowing call (and the bug class
54
+ * where forgetting that call fails open against a `null` actor).
55
+ *
56
+ * The runtime binding is identical to `rpc_action` — both register the
57
+ * same `RpcAction` shape on the action map. Only the compile-time
58
+ * handler signature differs.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * rpc_actor_action(permit_revoke_action_spec, async (input, ctx) => {
63
+ * // ctx.auth is RequestActorContext — no require_request_actor() needed.
64
+ * const revoker_id = ctx.auth.actor.id;
65
+ * // ...
66
+ * });
67
+ * ```
68
+ */
69
+ export const rpc_actor_action = (spec, handler) => ({
70
+ spec,
71
+ handler: handler,
72
+ });
46
73
  const jsonrpc_error_response = (id, error) => ({
47
74
  jsonrpc: JSONRPC_VERSION,
48
75
  id,
49
76
  error,
50
77
  });
51
78
  /**
52
- * Check auth for an action spec against the request context.
79
+ * Pre-validation auth gate fires before input validation so missing
80
+ * credentials short-circuit with `unauthenticated` instead of leaking
81
+ * a `invalid_params` error for methods with required input.
53
82
  *
54
- * @returns a JSON-RPC error object if auth fails, or `null` if authorized
83
+ * Reads `c.var.auth_account_id` (set by the auth middleware). Returns
84
+ * `unauthenticated` when `auth !== 'public'` and no account is on the
85
+ * request. Role / keeper checks are deferred until after the
86
+ * authorization phase populates the request context — see
87
+ * `check_action_auth_post_authorization`.
88
+ *
89
+ * @returns a JSON-RPC error object if no account is on the request, or `null`
55
90
  */
56
- const check_action_auth = (auth, request_context, credential_type) => {
91
+ const check_action_auth_pre_validation = (auth, account_id) => {
57
92
  if (auth === 'public')
58
93
  return null;
59
- if (!request_context)
94
+ if (account_id == null)
60
95
  return jsonrpc_error_messages.unauthenticated();
61
- if (auth === 'authenticated')
96
+ return null;
97
+ };
98
+ /**
99
+ * Post-authorization auth gate — fires after the dispatcher's authorization
100
+ * phase has populated `REQUEST_CONTEXT_KEY` with the resolved actor +
101
+ * permits. Enforces `role` and `keeper` requirements; `'public'` and
102
+ * `'authenticated'` already cleared the pre-validation gate.
103
+ *
104
+ * @returns a JSON-RPC error object if permit / credential check fails, or `null`
105
+ */
106
+ const check_action_auth_post_authorization = (auth, request_context, credential_type) => {
107
+ if (auth === 'public' || auth === 'authenticated')
62
108
  return null;
63
109
  if (auth === 'keeper') {
64
110
  // keeper requires daemon_token credential type AND the keeper role.
@@ -94,9 +140,18 @@ const check_action_auth = (auth, request_context, credential_type) => {
94
140
  * 1. **Parse envelope** — POST: JSON body as `JsonrpcRequest`. GET: `method`
95
141
  * and `params` from query string.
96
142
  * 2. **Lookup method** — find the `RpcAction` by method name.
97
- * 3. **Auth check** — verify identity against the action's `auth` requirement.
98
- * 4. **Validate params** parse input against the action's `input` schema.
99
- * 5. **Dispatch** — acquire DB handle (transaction for mutations, pool for reads),
143
+ * 3. **Pre-validation auth** — short-circuit `unauthenticated` when no
144
+ * account is on the request, before input validation runs.
145
+ * 4. **Authorization phase** — resolve the acting actor (when the action's
146
+ * auth requires permits or its input declares `acting?: ActingActor`)
147
+ * and build the request context. Runs before input validation so
148
+ * permit-grain auth checks return 403 before 400 invalid_params;
149
+ * `acting` is read from raw params via a string typeguard.
150
+ * 5. **Post-authorization auth** — enforce role / keeper requirements
151
+ * against the request context.
152
+ * 6. **Validate params** — parse input against the action's `input` schema.
153
+ * 7. **Rate limit** — per-action IP / account throttling.
154
+ * 8. **Dispatch** — acquire DB handle (transaction for mutations, pool for reads),
100
155
  * construct `ActionContext`, call handler, return JSON-RPC response.
101
156
  *
102
157
  * GET is restricted to `side_effects: false` actions (cacheable reads).
@@ -115,7 +170,6 @@ const check_action_auth = (auth, request_context, credential_type) => {
115
170
  */
116
171
  export const create_rpc_endpoint = (options) => {
117
172
  const { path: endpoint_path, actions, log, action_ip_rate_limiter = null, action_account_rate_limiter = null, } = options;
118
- // build action lookup map
119
173
  const action_map = new Map();
120
174
  for (const action of actions) {
121
175
  if (action_map.has(action.spec.method)) {
@@ -151,20 +205,98 @@ export const create_rpc_endpoint = (options) => {
151
205
  }));
152
206
  return c.json(error, jsonrpc_error_code_to_http_status(JSONRPC_ERROR_CODES.invalid_request));
153
207
  }
154
- // step 3: auth check
208
+ // step 3: pre-validation auth — short-circuit with `unauthenticated`
209
+ // when no account is on the request before input validation runs,
210
+ // so callers without credentials don't see `invalid_params` for
211
+ // methods with required input.
212
+ const action_auth = action.spec.auth;
213
+ const account_id = c.get(ACCOUNT_ID_KEY) ?? null;
214
+ const pre_validation_auth_error = check_action_auth_pre_validation(action_auth, account_id);
215
+ if (pre_validation_auth_error) {
216
+ const error = jsonrpc_error_response(id, pre_validation_auth_error);
217
+ return c.json(error, jsonrpc_error_code_to_http_status(pre_validation_auth_error.code));
218
+ }
219
+ // step 4: authorization phase — resolves the acting actor and
220
+ // builds the request context. Runs before input validation so
221
+ // permit-grain auth checks (`role` / `keeper`) surface 403
222
+ // before 400 invalid_params. `acting` is read from raw params
223
+ // (string typeguard) so multi-actor callers can still pick a
224
+ // persona without paying for full validation up front; an
225
+ // invalid `acting` shape will be rejected by step 5's input
226
+ // validation if it survives the authorization probe.
227
+ //
228
+ // Resolution failures come back as `{status, body}` so this
229
+ // dispatcher can fold them into a JSON-RPC error envelope —
230
+ // REST emits the same `body` directly. The reason string lands
231
+ // on `error.message` and `error.data.reason`; remaining
232
+ // diagnostic fields (e.g. `available[]` for `actor_required`)
233
+ // flatten under `error.data` so wire callers see structured
234
+ // data instead of a status-coded synthetic envelope.
235
+ if (action_auth !== 'public') {
236
+ const declares_acting = input_schema_declares_acting(action.spec.input);
237
+ const needs_actor = is_actor_implying_auth(action_auth) || declares_acting;
238
+ const raw_acting = declares_acting && typeof raw_params === 'object' && raw_params !== null
239
+ ? raw_params.acting
240
+ : undefined;
241
+ const acting_value = typeof raw_acting === 'string' ? raw_acting : undefined;
242
+ const failure = await apply_authorization_phase({ db: route.db }, c, needs_actor, acting_value);
243
+ if (failure) {
244
+ // `error.code` comes from `http_status_to_jsonrpc_error_code(failure.status)` so the
245
+ // wire shape stays uniform with every other JSON-RPC failure path. The 400 mapping
246
+ // lands on `invalid_params` even though `actor_required` / `actor_not_on_account`
247
+ // are not strictly "params malformed" failures — the alternative would be inventing
248
+ // a JSON-RPC code outside the http-status mapping just for these two reasons. The
249
+ // slight semantic mismatch is acceptable because consumers key on
250
+ // `error.data.reason`, never on `error.code` (the in-tree consumers — zzz, tx,
251
+ // visiones, mageguild — never match on the actor reason strings via `error.code`).
252
+ // The 500 mapping (`internal_error`) for `no_actors_on_account` / `account_vanished`
253
+ // is on-the-nose.
254
+ const { error: reason, ...rest } = failure.body;
255
+ const code = http_status_to_jsonrpc_error_code(failure.status);
256
+ const error = jsonrpc_error_response(id, {
257
+ code,
258
+ message: reason,
259
+ data: { reason, ...rest },
260
+ });
261
+ return c.json(error, failure.status);
262
+ }
263
+ }
264
+ // step 5: post-authorization auth — gate role / keeper requirements
265
+ // against the request context populated by the authorization phase.
155
266
  const request_context = get_request_context(c);
156
267
  const credential_type = c.get(CREDENTIAL_TYPE_KEY) ?? null;
157
- const auth_error = check_action_auth(action.spec.auth, request_context, credential_type);
158
- if (auth_error) {
159
- const error = jsonrpc_error_response(id, auth_error);
160
- return c.json(error, jsonrpc_error_code_to_http_status(auth_error.code));
268
+ const post_authorization_auth_error = check_action_auth_post_authorization(action_auth, request_context, credential_type);
269
+ if (post_authorization_auth_error) {
270
+ const error = jsonrpc_error_response(id, post_authorization_auth_error);
271
+ return c.json(error, jsonrpc_error_code_to_http_status(post_authorization_auth_error.code));
272
+ }
273
+ // step 6: validate params
274
+ // Missing `params` on the envelope maps to `undefined` for `z.void()`
275
+ // input schemas and `{}` for object inputs (matches HTTP's "empty
276
+ // body = empty object" convention so callers of all-optional-object
277
+ // RPC methods can omit `params` on the wire). JSON-RPC 2.0 §4.2
278
+ // forbids `params: null`, so `z.void()` is the spec-correct schema
279
+ // for parameterless methods — registration above rejects `z.null()`
280
+ // inputs to keep this branch from having to consider that legacy
281
+ // shape. When `raw_params` is present it flows through unchanged so
282
+ // contract-violating shapes still fail validation.
283
+ const params = is_void_schema(action.spec.input) ? raw_params : (raw_params ?? {});
284
+ const parse_result = action.spec.input.safeParse(params);
285
+ if (!parse_result.success) {
286
+ const error = jsonrpc_error_response(id, jsonrpc_error_messages.invalid_params('invalid params', {
287
+ issues: parse_result.error.issues,
288
+ }));
289
+ return c.json(error, jsonrpc_error_code_to_http_status(JSONRPC_ERROR_CODES.invalid_params));
161
290
  }
162
- // step 3.5: rate limit — throttle-requests semantics (record on every
291
+ // step 7: rate limit — throttle-requests semantics (record on every
163
292
  // invocation, no success-reset). Suits admin mutation oracles where
164
293
  // the *successful* call is the threat. Different from REST login's
165
294
  // throttle-failures pattern that resets on success. Silent partial
166
295
  // enforcement: a key is checked iff its bucket's limiter is wired —
167
296
  // `rate_limit: 'both'` with only one limiter set runs only that side.
297
+ // Account-keyed limiting bills the authenticated account: every
298
+ // authenticated action has `request_context.account.id`, regardless
299
+ // of whether an actor was resolved.
168
300
  const rate_limit = action.spec.rate_limit;
169
301
  const client_ip = get_client_ip(c);
170
302
  if (rate_limit) {
@@ -182,34 +314,16 @@ export const create_rpc_endpoint = (options) => {
182
314
  return reject(result.retry_after);
183
315
  }
184
316
  if (account_check) {
185
- const result = action_account_rate_limiter.check(request_context.actor.id);
317
+ const result = action_account_rate_limiter.check(request_context.account.id);
186
318
  if (!result.allowed)
187
319
  return reject(result.retry_after);
188
320
  }
189
321
  if (ip_check)
190
322
  action_ip_rate_limiter.record(client_ip);
191
323
  if (account_check)
192
- action_account_rate_limiter.record(request_context.actor.id);
193
- }
194
- // step 4: validate params
195
- // Missing `params` on the envelope maps to `undefined` for `z.void()`
196
- // input schemas and `{}` for object inputs (matches HTTP's "empty
197
- // body = empty object" convention so callers of all-optional-object
198
- // RPC methods can omit `params` on the wire). JSON-RPC 2.0 §4.2
199
- // forbids `params: null`, so `z.void()` is the spec-correct schema
200
- // for parameterless methods — registration above rejects `z.null()`
201
- // inputs to keep this branch from having to consider that legacy
202
- // shape. When `raw_params` is present it flows through unchanged so
203
- // contract-violating shapes still fail validation.
204
- const params = is_void_schema(action.spec.input) ? raw_params : (raw_params ?? {});
205
- const parse_result = action.spec.input.safeParse(params);
206
- if (!parse_result.success) {
207
- const error = jsonrpc_error_response(id, jsonrpc_error_messages.invalid_params('invalid params', {
208
- issues: parse_result.error.issues,
209
- }));
210
- return c.json(error, jsonrpc_error_code_to_http_status(JSONRPC_ERROR_CODES.invalid_params));
324
+ action_account_rate_limiter.record(request_context.account.id);
211
325
  }
212
- // step 5: dispatch — transaction for mutations, pool for reads
326
+ // step 8: dispatch — transaction for mutations, pool for reads
213
327
  const use_transaction = action.spec.side_effects;
214
328
  const notify = (notify_method, _notify_params) => {
215
329
  if (DEV) {
@@ -251,13 +365,13 @@ export const create_rpc_endpoint = (options) => {
251
365
  // Duck-type check: Error with numeric `code` signals a JSON-RPC error.
252
366
  // Avoids instanceof which fails when consumers throw their own ThrownJsonrpcError
253
367
  // (structurally identical but different class identity, e.g. zzz's copy).
254
- if (err instanceof Error && typeof err.code === 'number') {
255
- const code = err.code;
256
- const data = err.data;
368
+ const error_like = err;
369
+ if (err instanceof Error && typeof error_like.code === 'number') {
370
+ const code = error_like.code;
257
371
  const status = jsonrpc_error_code_to_http_status(code);
258
372
  const error_json = { code, message: err.message };
259
- if (data !== undefined)
260
- error_json.data = data;
373
+ if (error_like.data !== undefined)
374
+ error_json.data = error_like.data;
261
375
  return c.json(jsonrpc_error_response(id, error_json), status);
262
376
  }
263
377
  // generic error
@@ -23,8 +23,8 @@
23
23
  * The consumer is responsible for rejecting unauthenticated upgrades *before*
24
24
  * routing to this handler (fuz_app's `require_auth` middleware, or
25
25
  * `register_ws_endpoint` which wires it for you). Inside the dispatcher,
26
- * `get_request_context(c)` is treated as guaranteed non-null and per-action
27
- * auth is enforced on each message.
26
+ * `require_request_context(c)` enforces the dispatcher invariant and
27
+ * per-action auth is enforced on each message.
28
28
  *
29
29
  * @module
30
30
  */
@@ -150,9 +150,9 @@ export interface RegisterActionWsOptions<TCtx extends BaseHandlerContext> {
150
150
  */
151
151
  action_ip_rate_limiter?: RateLimiter | null;
152
152
  /**
153
- * Per-actor rate limiter consulted for actions whose spec declares
153
+ * Per-account rate limiter consulted for actions whose spec declares
154
154
  * `rate_limit: 'account'` or `'both'`. Keyed on
155
- * `request_context.actor.id`. `null` (or omitted) disables the
155
+ * `request_context.account.id`. `null` (or omitted) disables the
156
156
  * account check. Same limiter is shared with the HTTP RPC dispatcher.
157
157
  */
158
158
  action_account_rate_limiter?: RateLimiter | null;