@fuzdev/fuz_app 0.54.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.
- package/dist/actions/CLAUDE.md +68 -13
- package/dist/actions/action_codegen.d.ts +13 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +15 -1
- package/dist/actions/action_rpc.d.ts +60 -7
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +158 -44
- package/dist/actions/register_action_ws.d.ts +4 -4
- package/dist/actions/register_action_ws.js +6 -6
- package/dist/actions/register_ws_endpoint.d.ts +20 -7
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +30 -5
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +0 -4
- package/dist/auth/CLAUDE.md +219 -66
- package/dist/auth/account_actions.d.ts +6 -6
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +8 -11
- package/dist/auth/account_queries.d.ts +6 -3
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +14 -5
- package/dist/auth/account_routes.d.ts +7 -10
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +70 -23
- package/dist/auth/account_schema.d.ts +19 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +20 -0
- package/dist/auth/admin_action_specs.d.ts +45 -11
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +23 -8
- package/dist/auth/admin_actions.d.ts +8 -7
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +11 -18
- package/dist/auth/audit_log_queries.d.ts +53 -14
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +45 -2
- package/dist/auth/audit_log_schema.d.ts +55 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +19 -3
- package/dist/auth/bearer_auth.d.ts +9 -7
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +13 -21
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +5 -0
- package/dist/auth/daemon_token_middleware.d.ts +23 -11
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +26 -20
- package/dist/auth/deps.d.ts +14 -0
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +4 -2
- package/dist/auth/migrations.d.ts +15 -7
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +15 -7
- package/dist/auth/permit_offer_action_specs.d.ts +45 -6
- package/dist/auth/permit_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/permit_offer_action_specs.js +38 -7
- package/dist/auth/permit_offer_actions.d.ts +2 -2
- package/dist/auth/permit_offer_actions.d.ts.map +1 -1
- package/dist/auth/permit_offer_actions.js +98 -90
- package/dist/auth/permit_offer_notifications.d.ts +10 -0
- package/dist/auth/permit_offer_notifications.d.ts.map +1 -1
- package/dist/auth/permit_offer_queries.d.ts +68 -9
- package/dist/auth/permit_offer_queries.d.ts.map +1 -1
- package/dist/auth/permit_offer_queries.js +147 -35
- package/dist/auth/permit_offer_schema.d.ts +23 -1
- package/dist/auth/permit_offer_schema.d.ts.map +1 -1
- package/dist/auth/permit_offer_schema.js +5 -0
- package/dist/auth/permit_queries.d.ts +17 -5
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +19 -8
- package/dist/auth/request_context.d.ts +321 -38
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +393 -66
- package/dist/auth/route_guards.d.ts +10 -4
- package/dist/auth/route_guards.d.ts.map +1 -1
- package/dist/auth/route_guards.js +14 -8
- package/dist/auth/self_service_role_action_specs.d.ts +2 -0
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +2 -0
- package/dist/auth/self_service_role_actions.d.ts +6 -5
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +18 -8
- package/dist/db/migrate.d.ts +11 -7
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +9 -6
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +5 -3
- package/dist/hono_context.d.ts +77 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +50 -0
- package/dist/http/CLAUDE.md +80 -17
- package/dist/http/error_schemas.d.ts +92 -1
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +73 -16
- package/dist/http/jsonrpc_errors.d.ts +27 -2
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +26 -2
- package/dist/http/route_spec.d.ts +62 -4
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +117 -21
- package/dist/http/schema_helpers.d.ts +13 -1
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +21 -2
- package/dist/http/surface.d.ts +10 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +2 -2
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +11 -1
- package/dist/testing/CLAUDE.md +23 -17
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +15 -13
- package/dist/testing/adversarial_headers.js +1 -1
- package/dist/testing/app_server.js +2 -2
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +21 -7
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +6 -3
- package/dist/testing/entities.d.ts +2 -1
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +1 -0
- package/dist/testing/integration_helpers.d.ts +4 -2
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +9 -5
- package/dist/testing/middleware.d.ts +12 -8
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +67 -25
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +3 -1
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +5 -1
- package/dist/ui/CLAUDE.md +16 -10
- package/dist/ui/PermitOfferForm.svelte +14 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts +6 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts +8 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +14 -3
- package/dist/ui/permit_offers_state.svelte.d.ts +9 -1
- package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -1
- package/dist/ui/permit_offers_state.svelte.js +7 -1
- package/package.json +1 -1
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -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.
|
|
69
|
-
|
|
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. **
|
|
242
|
-
5. **
|
|
243
|
-
6. **
|
|
244
|
-
7. **
|
|
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
|
|
288
|
-
rpc_action(
|
|
289
|
-
rpc_action(
|
|
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
|
-
`
|
|
301
|
-
|
|
302
|
-
|
|
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":"AAGA,OAAO,KAAK,EAAC,eAAe,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAGxE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,kCAAmC,CAAC;AAExE,8FAA8F;AAC9F,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAI5E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GAAI,QAAQ,MAAM,KAAG,MAAM,IAAI,oBACrC,CAAC;AAEjC,UAAU,UAAU;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACzB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAa;IAE1D;;;;;OAKG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQrC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1C,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAOrD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IA6BtD;;;;OAIG;IACH,KAAK,IAAI,MAAM;IAIf,WAAW,IAAI,OAAO;IAItB,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,0FAA0F;IAC1F,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC;IAIxB;;;;;OAKG;IACH,KAAK,IAAI,IAAI;CAiDb;AAED,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB,GAC/B,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,KAC9B,KAAK,CAAC,gBAAgB,CA4DxB,CAAC;AAEF,gHAAgH;AAChH,eAAO,MAAM,wBAAwB,4BAA4B,CAAC;AAElE,4FAA4F;AAC5F,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AAExD,sGAAsG;AACtG,eAAO,MAAM,sBAAsB,0BAA0B,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,OAAO,gBAAgB,EACvB,SAAS,aAAa,EACtB,mBAAkB,MAAiC,KACjD,MAkBF,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,EAChC,SAAS,aAAa,EACtB,UAAU;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAC,KAC/D,MA2BF,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,MACU,CAAC;AAG/D,eAAO,MAAM,yBAAyB,GAAI,QAAQ,MAAM,KAAG,MAAiC,CAAC;AAC7F,eAAO,MAAM,+BAA+B,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAC9C,eAAO,MAAM,gCAAgC,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAE/C
|
|
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${
|
|
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-
|
|
163
|
+
* Per-account rate limiter consulted for actions whose spec declares
|
|
122
164
|
* `rate_limit: 'account'` or `'both'`. Keyed on
|
|
123
|
-
* `request_context.
|
|
124
|
-
*
|
|
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. **
|
|
138
|
-
*
|
|
139
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
91
|
+
const check_action_auth_pre_validation = (auth, account_id) => {
|
|
57
92
|
if (auth === 'public')
|
|
58
93
|
return null;
|
|
59
|
-
if (
|
|
94
|
+
if (account_id == null)
|
|
60
95
|
return jsonrpc_error_messages.unauthenticated();
|
|
61
|
-
|
|
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. **
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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
|
|
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
|
|
158
|
-
if (
|
|
159
|
-
const error = jsonrpc_error_response(id,
|
|
160
|
-
return c.json(error, jsonrpc_error_code_to_http_status(
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
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
|
-
* `
|
|
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-
|
|
153
|
+
* Per-account rate limiter consulted for actions whose spec declares
|
|
154
154
|
* `rate_limit: 'account'` or `'both'`. Keyed on
|
|
155
|
-
* `request_context.
|
|
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;
|