@fuzdev/fuz_app 0.148.0 → 0.150.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 +11 -9
- package/dist/actions/action_bridge.d.ts +7 -0
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_bridge.js +10 -0
- package/dist/actions/action_event.d.ts.map +1 -1
- package/dist/actions/action_event.js +14 -1
- package/dist/actions/action_event_types.d.ts +1 -1
- package/dist/actions/action_event_types.d.ts.map +1 -1
- package/dist/actions/perform_action.d.ts +17 -1
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +30 -7
- package/dist/actions/register_action_ws.d.ts +8 -2
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +24 -4
- package/dist/actions/transports_ws_auth_guard.d.ts +6 -4
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +9 -6
- package/dist/actions/ws_endpoint_spec.d.ts +1 -1
- package/dist/auth/CLAUDE.md +72 -33
- package/dist/auth/account_action_specs.d.ts +12 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +16 -2
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +7 -4
- package/dist/auth/actor_lookup_queries.d.ts +3 -2
- package/dist/auth/actor_lookup_queries.d.ts.map +1 -1
- package/dist/auth/actor_lookup_queries.js +6 -3
- package/dist/auth/actor_search_queries.d.ts +5 -2
- package/dist/auth/actor_search_queries.d.ts.map +1 -1
- package/dist/auth/actor_search_queries.js +11 -4
- package/dist/auth/admin_action_specs.d.ts +4 -4
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +33 -12
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +35 -6
- package/dist/auth/all_cell_actions.d.ts +11 -2
- package/dist/auth/all_cell_actions.d.ts.map +1 -1
- package/dist/auth/all_cell_actions.js +2 -1
- package/dist/auth/audit_emitter.d.ts +23 -3
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +14 -4
- package/dist/auth/audit_log_schema.d.ts +1 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +9 -5
- package/dist/auth/bootstrap_account.d.ts +25 -1
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +37 -4
- package/dist/auth/bootstrap_routes.d.ts +3 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +5 -4
- package/dist/auth/cell_action_specs.d.ts +215 -15
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +103 -16
- package/dist/auth/cell_actions.d.ts +40 -18
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +126 -19
- package/dist/auth/cell_audit_events.d.ts +1 -1
- package/dist/auth/cell_audit_events.d.ts.map +1 -1
- package/dist/auth/cell_audit_events.js +3 -2
- package/dist/auth/cell_audit_metadata.d.ts +11 -0
- package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
- package/dist/auth/cell_audit_metadata.js +10 -0
- package/dist/auth/cell_grant_action_specs.d.ts +7 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_grant_action_specs.js +7 -0
- package/dist/auth/cell_grant_actions.d.ts +36 -4
- package/dist/auth/cell_grant_actions.d.ts.map +1 -1
- package/dist/auth/cell_grant_actions.js +20 -3
- package/dist/auth/credential_type_schema.d.ts +3 -2
- package/dist/auth/credential_type_schema.d.ts.map +1 -1
- package/dist/auth/credential_type_schema.js +3 -2
- package/dist/auth/role_grant_offer_action_specs.d.ts +18 -2
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +30 -6
- package/dist/auth/role_grant_offer_actions.d.ts +9 -4
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +58 -7
- package/dist/auth/role_grant_offer_notifications.d.ts +3 -2
- package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_notifications.js +3 -2
- package/dist/auth/role_grant_offer_queries.d.ts +20 -0
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +35 -0
- package/dist/auth/role_grant_queries.d.ts +27 -9
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +40 -13
- package/dist/auth/role_schema.d.ts +14 -8
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +6 -3
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +20 -0
- package/dist/db/CLAUDE.md +9 -2
- package/dist/db/cell_ddl.d.ts +9 -1
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +11 -1
- package/dist/db/cell_queries.d.ts +53 -7
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +61 -7
- package/dist/http/CLAUDE.md +14 -2
- package/dist/http/body_limit.d.ts +3 -1
- package/dist/http/body_limit.d.ts.map +1 -1
- package/dist/http/body_limit.js +6 -1
- package/dist/http/error_schemas.d.ts +8 -0
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -0
- package/dist/http/jsonrpc.d.ts +9 -0
- package/dist/http/jsonrpc.d.ts.map +1 -1
- package/dist/http/jsonrpc.js +17 -0
- package/dist/http/jsonrpc_errors.d.ts +40 -15
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +99 -35
- package/dist/http/proxy.d.ts +4 -2
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +7 -2
- package/dist/http/surface.d.ts +3 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +7 -4
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +3 -2
- package/dist/schema_meta.d.ts +92 -1
- package/dist/schema_meta.d.ts.map +1 -1
- package/dist/schema_meta.js +117 -0
- package/dist/server/app_server.d.ts +13 -5
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +32 -8
- package/dist/testing/CLAUDE.md +196 -82
- package/dist/testing/adversarial_input_cases.d.ts +67 -7
- package/dist/testing/adversarial_input_cases.d.ts.map +1 -1
- package/dist/testing/adversarial_input_cases.js +210 -165
- package/dist/testing/app_server.d.ts +3 -1
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.d.ts +3 -2
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +5 -3
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/actor_lookup.js +27 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts +6 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/audit_cross_helpers.js +11 -0
- package/dist/testing/cross_backend/capabilities.d.ts +29 -5
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +10 -6
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +117 -2
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +109 -13
- package/dist/testing/cross_backend/cell_moderate.d.ts +7 -1
- package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_moderate.js +68 -4
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_ref_admission.js +18 -9
- package/dist/testing/cross_backend/conformance_table.d.ts +5 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +12 -10
- package/dist/testing/cross_backend/cross_rpc.d.ts +3 -1
- package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -1
- package/dist/testing/cross_backend/cross_rpc.js +2 -2
- package/dist/testing/cross_backend/default_backend_configs.d.ts +9 -7
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +3 -3
- package/dist/testing/cross_backend/differential.d.ts +7 -0
- package/dist/testing/cross_backend/differential.d.ts.map +1 -1
- package/dist/testing/cross_backend/differential.js +17 -0
- package/dist/testing/cross_backend/expected_schema.json +1 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +5 -4
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +74 -2
- package/dist/testing/cross_backend/rpc_adversarial.d.ts +9 -0
- package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -1
- package/dist/testing/cross_backend/rpc_adversarial.js +16 -6
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
- package/dist/testing/cross_backend/setup.d.ts +2 -2
- package/dist/testing/cross_backend/spine_suites.d.ts +85 -5
- package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_suites.js +291 -20
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +15 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +24 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +18 -19
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +66 -42
- package/dist/testing/cross_backend/testing_backdoor.d.ts +27 -4
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +136 -41
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +30 -15
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +52 -35
- package/dist/testing/cross_backend/transport_composition.js +3 -3
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +6 -3
- package/dist/testing/rpc_attack_surface.d.ts +10 -0
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +15 -3
- package/dist/testing/rpc_envelope_cases.d.ts +5 -2
- package/dist/testing/rpc_envelope_cases.d.ts.map +1 -1
- package/dist/testing/rpc_envelope_cases.js +4 -1
- package/dist/testing/rpc_round_trip.js +3 -3
- package/dist/testing/schema_generators.d.ts +99 -2
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +244 -76
- package/dist/testing/stubs.d.ts +4 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/zod.d.ts +9 -0
- package/dist/zod.d.ts.map +1 -1
- package/dist/zod.js +20 -0
- package/package.json +1 -1
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -70,7 +70,8 @@ dropping per-spec `*_METHOD` constants (readers dereference `.method`). See
|
|
|
70
70
|
- `local_call` — none (no REST, no RPC, no WS, no SSE).
|
|
71
71
|
|
|
72
72
|
`create_action_route_spec` throws if `spec.auth` is null (notifications and
|
|
73
|
-
local calls cannot become routes)
|
|
73
|
+
local calls cannot become routes) or `spec.rate_limit` is set (a bridged route
|
|
74
|
+
never reaches the `perform_action` limiters). `create_action_event_spec` throws on any
|
|
74
75
|
non-`remote_notification` kind.
|
|
75
76
|
|
|
76
77
|
## Registry compile (`actions/compile_action_registry.ts`)
|
|
@@ -177,7 +178,7 @@ and `FrontendActionHandlers`.
|
|
|
177
178
|
Derives transport-specific specs from action specs. HTTP-specific concerns
|
|
178
179
|
(path, handler, errors) come from options, not the action spec.
|
|
179
180
|
|
|
180
|
-
- `create_action_route_spec(spec, options)` — one action → one `RouteSpec`. HTTP method defaults by `side_effects` (`true` → POST, `false` → GET; override via `options.http_method`). `route.auth` is `spec.auth` plus a derived `required_scope`. `transaction: spec.side_effects`. Throws if `spec.auth` is null.
|
|
181
|
+
- `create_action_route_spec(spec, options)` — one action → one `RouteSpec`. HTTP method defaults by `side_effects` (`true` → POST, `false` → GET; override via `options.http_method`). `route.auth` is `spec.auth` plus a derived `required_scope`. `transaction: spec.side_effects`. Throws if `spec.auth` is null, or if `spec.rate_limit` is set (the action limiters run in `perform_action`, which a bridged route never reaches).
|
|
181
182
|
|
|
182
183
|
**The bridge carries the token-scope gate across.** A bridged route runs
|
|
183
184
|
`options.handler` through the REST pipeline and never reaches
|
|
@@ -235,9 +236,10 @@ Shim responsibilities (per-request):
|
|
|
235
236
|
6. Bind result via `perform_action_result_to_envelope(id, result)`; `c.json(envelope, result.status)`.
|
|
236
237
|
|
|
237
238
|
Error paths: `ThrownJsonrpcError` (duck-typed via `err instanceof Error &&
|
|
238
|
-
|
|
239
|
+
is_jsonrpc_error_code(err.code)` to handle cross-copy `instanceof` misses,
|
|
239
240
|
e.g. when consumers like zzz throw their own `ThrownJsonrpcError`)
|
|
240
|
-
preserves code + data verbatim
|
|
241
|
+
preserves code + data verbatim; an error whose numeric `code` is not a
|
|
242
|
+
JSON-RPC one (a `DOMException`) takes the generic path. Generic throws become `internal_error` 500;
|
|
241
243
|
message is the raw error under `DEV`, "internal server error" otherwise.
|
|
242
244
|
|
|
243
245
|
A result `c.json` cannot serialize (a `bigint`, a cycle) answers the same
|
|
@@ -308,7 +310,7 @@ Pipeline (401 → authz → 403 → 429 → 400 → handler):
|
|
|
308
310
|
2. Authorization phase — `apply_authorization_phase` against `account_id` + `read_acting(auth, raw_params)`, which takes the selector off the raw params (malformed reads as omitted) since validation runs later. Test escape hatch via `preset.request_context`
|
|
309
311
|
3. Post-authorization auth (403) — credential-type gate, then token scope, then role
|
|
310
312
|
4. Rate limit (429) — throttle-requests semantics, ahead of validation so malformed params charge the budget and a throttled caller pays no schema work
|
|
311
|
-
5. Validate params (400) — `spec.input.safeParse` with `z.void()` / `?? {}` rules; a `z.void()` input reads absent, `null` (GET `?params=null`), and `{}` alike as the no-arg call (`to_input_params` in `http/schema_helpers.ts`, via `to_void_params`, the twin of the Rust `require_void_params`; the peer receive path in `ActionDispatcher` runs the same normalizer) and refuses any other shape
|
|
313
|
+
5. Validate params (400) — `spec.input.safeParse` with `z.void()` / `?? {}` rules; a `z.void()` input reads absent, `null` (GET `?params=null`), and `{}` alike as the no-arg call (`to_input_params` in `http/schema_helpers.ts`, via `to_void_params`, the twin of the Rust `require_void_params`; the peer receive path in `ActionDispatcher` runs the same normalizer) and refuses any other shape. A refusal is `to_invalid_params_error`: the generic `invalid params`, `data.reason` when the first issue's field declares an `invalid_reason` in its `.meta()` and no issue sits on the input itself, such as an unknown key (`resolve_issues_invalid_reason`, `schema_meta.ts` — see ../../../docs/architecture.md §Error Schema System), and the Zod issues DEV-only
|
|
312
314
|
6. Dispatch + DEV output validation + error normalization — `spec.side_effects` picks transaction vs pool. `ThrownJsonrpcError` preserves code + data; generic throws become `internal_error`
|
|
313
315
|
|
|
314
316
|
`PerformActionInput` carries `account_id`, `credential_type`, `client_ip`,
|
|
@@ -360,7 +362,7 @@ and `allow_fallback: boolean` (default `true`). Explicit
|
|
|
360
362
|
|
|
361
363
|
### WS close codes (`actions/transports.ts`)
|
|
362
364
|
|
|
363
|
-
- `WS_CLOSE_SESSION_REVOKED = 4001` — server revoked auth (reason `WS_CLOSE_SESSION_REVOKED_REASON`, `Session revoked`); client enters permanent `revoked` state, no reconnect. Also the close for an upgrade refused at admission because its credential was revoked mid-upgrade — indistinguishable on the wire by design — and for a socket whose credential expired while open, with reason `WS_CLOSE_CREDENTIAL_EXPIRED_REASON` (`Credential expired`), which the client treats as the same `revoked` state.
|
|
365
|
+
- `WS_CLOSE_SESSION_REVOKED = 4001` — server revoked auth (reason `WS_CLOSE_SESSION_REVOKED_REASON`, `Session revoked`); client enters permanent `revoked` state, no reconnect. Also the close for an upgrade refused at admission because its credential was revoked, or its acting actor soft-deleted, mid-upgrade — indistinguishable on the wire by design — and for a socket whose credential expired while open, with reason `WS_CLOSE_CREDENTIAL_EXPIRED_REASON` (`Credential expired`), which the client treats as the same `revoked` state.
|
|
364
366
|
- `WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT = 4002` — client observed receive-silence past its receive timeout (`resolve_heartbeat_receive_timeout`).
|
|
365
367
|
- `WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT = 4003` — server observed receive-silence past `DEFAULT_SERVER_HEARTBEAT_TIMEOUT` (60s).
|
|
366
368
|
- `WS_CLOSE_CONNECTION_LIMIT = 4004` — server closed this socket to admit a newer one on the same account, past the per-account connection cap (reason `connection limit`). Not a revocation: the client stays `closed` with `superseded` set and no reconnect, until `connect()` is called. Twin of the Rust spine's constant.
|
|
@@ -736,8 +738,8 @@ guard reads too — one declaration, so the two can't drift:
|
|
|
736
738
|
- `none` → nothing — every other event, and any event type fuz_app doesn't
|
|
737
739
|
define
|
|
738
740
|
|
|
739
|
-
`outcome === '
|
|
740
|
-
identifiers. Reacting to them would let an authenticated caller close
|
|
741
|
+
Only `outcome === 'success'` events act — a failure carries attacker-controlled
|
|
742
|
+
identifiers, and an outcome outside the enum is no revocation. Reacting to them would let an authenticated caller close
|
|
741
743
|
another user's socket by guessing a session hash or token id.
|
|
742
744
|
|
|
743
745
|
## WebSocket dispatch — three layered entry points
|
|
@@ -1051,7 +1053,7 @@ high-level shapes that span modules:
|
|
|
1051
1053
|
- **39-variant discriminated union** — `ActionEventDataUnion<TMethod, TInput, TOutput>` across `kind` + `phase` + `step` (28 for `request_response`, 6 for `remote_notification`, 5 for `local_call`). Narrows `input` / `output` / `error` / `request` / `response` / `notification` / `progress` at each lifecycle point.
|
|
1052
1054
|
- **Step transitions** — `initial → parsed | failed`, `parsed → handling | failed`, `handling → handled | failed`, `handled`/`failed` terminal. `validate_step_transition(from, to)` throws on illegal moves.
|
|
1053
1055
|
- **Phase transitions** — chained: `send_request → receive_response`, `receive_request → send_response`; everything else terminal. `validate_phase_for_kind` + `validate_phase_transition` enforce.
|
|
1054
|
-
- **`ActionEvent.parse()`** — `initial → parsed` via `spec.input.safeParse`. Input validation failures **fail immediately** without routing through an error phase (client-side programming errors, not runtime conditions with handlers). Handler errors DO route through `send_error` / `receive_error`. On `receive_response` with error response, transitions to `receive_error` instead of failing; a success response's `result` is validated against `spec.output` (always, not DEV-only — it's inbound data; undeclared keys are dropped rather than refused, so an older client survives an added response field) and `output` becomes the parsed value, or the event moves to `receive_error` with an `internal_error` carrying `data: {reason: ERROR_RESPONSE_OUTPUT_INVALID, validation_errors}` — a remote contract break, deliberately distinct from the `invalid_params` / `validation_error` of bad caller input.
|
|
1056
|
+
- **`ActionEvent.parse()`** — `initial → parsed` via `spec.input.safeParse`. Input validation failures **fail immediately** without routing through an error phase (client-side programming errors, not runtime conditions with handlers); their `invalid_params` `data` is `{validation_errors, reason?}`, `reason` the declared `invalid_reason` the server would answer. Handler errors DO route through `send_error` / `receive_error`. On `receive_response` with error response, transitions to `receive_error` instead of failing; a success response's `result` is validated against `spec.output` (always, not DEV-only — it's inbound data; undeclared keys are dropped rather than refused, so an older client survives an added response field) and `output` becomes the parsed value, or the event moves to `receive_error` with an `internal_error` carrying `data: {reason: ERROR_RESPONSE_OUTPUT_INVALID, validation_errors}` — a remote contract break, deliberately distinct from the `invalid_params` / `validation_error` of bad caller input.
|
|
1055
1057
|
- **Handler throws** — `ThrownJsonrpcError` keeps code/message/data; any other throw becomes `internal_error` carrying the thrown message. When the error answers a remote caller (`receive_request` / `send_response`), the raw message goes through `dev_only` like the server dispatch core.
|
|
1056
1058
|
- **Protocol message creation is automatic** — transitioning `parsed → handling` on `send_request` materializes the outgoing `JsonrpcRequest` with a fresh `create_uuid()` id; on `send` (notification) it materializes the `JsonrpcNotification`.
|
|
1057
1059
|
|
|
@@ -80,11 +80,18 @@ export declare const derive_http_method: (side_effects: ActionSideEffects) => Ro
|
|
|
80
80
|
* same holder reaches the route by dropping the credential, so the guard would
|
|
81
81
|
* enforce nothing — the shape `fuz_auth_guard_resolver` refuses outright.
|
|
82
82
|
*
|
|
83
|
+
* **A rate-limited action is refused.** The action limiters run in
|
|
84
|
+
* `perform_action`, which a bridged route never reaches, and a route spec has
|
|
85
|
+
* no limiter of its own — so bridging a `rate_limit` action would serve it
|
|
86
|
+
* unthrottled beside the throttled RPC and WS paths, a way around its budget.
|
|
87
|
+
* Serve it over RPC or WS instead.
|
|
88
|
+
*
|
|
83
89
|
* @param spec - the action spec (must have non-null `auth`)
|
|
84
90
|
* @param options - HTTP-specific options (path, handler, optional overrides)
|
|
85
91
|
* @returns a `RouteSpec` ready for `apply_route_specs`
|
|
86
92
|
* @throws Error if `spec.auth` is null (only `request_response` actions can
|
|
87
93
|
* become routes; notifications and local calls cannot)
|
|
94
|
+
* @throws Error if `spec.rate_limit` is set (the bridged route would be unthrottled)
|
|
88
95
|
*/
|
|
89
96
|
export declare const create_action_route_spec: (spec: ActionSpec, options: ActionRouteOptions) => RouteSpec;
|
|
90
97
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_bridge.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,uGAAuG;IACvG,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,8GAA8G;IAC9G,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,GAAI,cAAc,iBAAiB,KAAG,WAEpE,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"action_bridge.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,uGAAuG;IACvG,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,8GAA8G;IAC9G,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,GAAI,cAAc,iBAAiB,KAAG,WAEpE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,UAAU,EAChB,SAAS,kBAAkB,KACzB,SAmCF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,UAAU,EAChB,UAAU,kBAAkB,KAC1B,SAcF,CAAC"}
|
|
@@ -46,16 +46,26 @@ export const derive_http_method = (side_effects) => {
|
|
|
46
46
|
* same holder reaches the route by dropping the credential, so the guard would
|
|
47
47
|
* enforce nothing — the shape `fuz_auth_guard_resolver` refuses outright.
|
|
48
48
|
*
|
|
49
|
+
* **A rate-limited action is refused.** The action limiters run in
|
|
50
|
+
* `perform_action`, which a bridged route never reaches, and a route spec has
|
|
51
|
+
* no limiter of its own — so bridging a `rate_limit` action would serve it
|
|
52
|
+
* unthrottled beside the throttled RPC and WS paths, a way around its budget.
|
|
53
|
+
* Serve it over RPC or WS instead.
|
|
54
|
+
*
|
|
49
55
|
* @param spec - the action spec (must have non-null `auth`)
|
|
50
56
|
* @param options - HTTP-specific options (path, handler, optional overrides)
|
|
51
57
|
* @returns a `RouteSpec` ready for `apply_route_specs`
|
|
52
58
|
* @throws Error if `spec.auth` is null (only `request_response` actions can
|
|
53
59
|
* become routes; notifications and local calls cannot)
|
|
60
|
+
* @throws Error if `spec.rate_limit` is set (the bridged route would be unthrottled)
|
|
54
61
|
*/
|
|
55
62
|
export const create_action_route_spec = (spec, options) => {
|
|
56
63
|
if (spec.auth === null) {
|
|
57
64
|
throw new Error(`Cannot derive route spec from action '${spec.method}': auth is null (only request_response actions with non-null auth can become routes)`);
|
|
58
65
|
}
|
|
66
|
+
if (spec.rate_limit) {
|
|
67
|
+
throw new Error(`Cannot derive route spec from action '${spec.method}': it declares rate_limit '${spec.rate_limit}', which a bridged route would not enforce (serve it over RPC or WS instead)`);
|
|
68
|
+
}
|
|
59
69
|
const auth = options.auth ?? spec.auth;
|
|
60
70
|
// Carry the dispatcher's per-method scope gate across — see the token-scope
|
|
61
71
|
// note above. An explicit capability wins; a public route has no credential
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_event.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_event.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAc,eAAe,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"action_event.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_event.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAc,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAiBtF,OAAO,KAAK,EACX,cAAc,EACd,sBAAsB,EACtB,mBAAmB,EAGnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AACvF,OAAO,EAAmB,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAapF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,EAAG,yBAAkC,CAAC;AAmBhF,MAAM,MAAM,yBAAyB,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,IAAI,CACxE,QAAQ,EAAE,oBAAoB,CAAC,OAAO,CAAC,EACvC,QAAQ,EAAE,oBAAoB,CAAC,OAAO,CAAC,EACvC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,KACvB,IAAI,CAAC;AAEV;;GAEG;AACH,qBAAa,WAAW,CACvB,OAAO,SAAS,MAAM,GAAG,MAAM,EAC/B,MAAM,SAAS,gBAAgB,GAAG,gBAAgB,EAClD,KAAK,SAAS,eAAe,GAAG,eAAe;;IAK/C,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAC;IAC7C;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IAErD,IAAI,IAAI,IAAI,oBAAoB,CAAC,OAAO,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAA;KAAE,CAEzE;gBAGA,WAAW,EAAE,sBAAsB,EACnC,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,oBAAoB,CAAC,OAAO,CAAC;IAOpC,MAAM,IAAI,oBAAoB,CAAC,OAAO,CAAC;IAMvC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,EAAE,yBAAyB,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI;IAKjE;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC,OAAO,CAAC,GAAG,IAAI;IAUvD;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,IAAI,IAAI;IAiEb;;;;;;;;;;OAUG;IAGG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAsCnC;;;;;;;OAOG;IACH,WAAW,IAAI,IAAI;IA8BnB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAezC,WAAW,IAAI,OAAO;IAItB,eAAe,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAIxC,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAQ1C,YAAY,CAAC,QAAQ,EAAE,sBAAsB,GAAG,IAAI;IAUpD,gBAAgB,CAAC,YAAY,EAAE,mBAAmB,GAAG,IAAI;CAgMzD;AAGD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,SAAS,MAAM,GAAG,MAAM,EAClE,aAAa,sBAAsB,EACnC,MAAM,eAAe,EACrB,OAAO,OAAO,EACd,gBAAgB,gBAAgB,KAC9B,WAAW,CAAC,OAAO,CAmBrB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GAAI,OAAO,SAAS,MAAM,GAAG,MAAM,EAC5E,MAAM,oBAAoB,CAAC,OAAO,CAAC,EACnC,aAAa,sBAAsB,KACjC,WAAW,CAAC,OAAO,CAOrB,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAC9B,UAAU,OAAO,EACjB,aAAa,sBAAsB,KACjC,WAGF,CAAC"}
|
|
@@ -12,6 +12,7 @@ import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
|
12
12
|
import { create_jsonrpc_request, create_jsonrpc_response, create_jsonrpc_error_response, create_jsonrpc_notification, to_jsonrpc_params, is_jsonrpc_error_response } from '../http/jsonrpc_helpers.js';
|
|
13
13
|
import { dev_only, jsonrpc_error_messages, ThrownJsonrpcError, UNKNOWN_ERROR_MESSAGE } from '../http/jsonrpc_errors.js';
|
|
14
14
|
import { safe_parse_dropping_unknown_keys } from '../http/schema_helpers.js';
|
|
15
|
+
import { resolve_issues_invalid_reason } from '../schema_meta.js';
|
|
15
16
|
import { ActionEventData } from './action_event_data.js';
|
|
16
17
|
import { validate_step_transition, validate_phase_transition, should_validate_output, is_action_complete, create_initial_data, get_initial_phase, is_request_response, is_send_request_with_parsed_input, is_notification_send_with_parsed_input } from './action_event_helpers.js';
|
|
17
18
|
/**
|
|
@@ -20,6 +21,18 @@ import { validate_step_transition, validate_phase_transition, should_validate_ou
|
|
|
20
21
|
* `invalid_params` / `validation_error` a caller's own bad input produces.
|
|
21
22
|
*/
|
|
22
23
|
export const ERROR_RESPONSE_OUTPUT_INVALID = 'response_output_invalid';
|
|
24
|
+
/**
|
|
25
|
+
* `error.data` for a local input parse failure: the issues, and the declared
|
|
26
|
+
* `invalid_reason` of the first issue's field — the reason the server would
|
|
27
|
+
* answer the same input with, so a caller matching `data.reason` reads one
|
|
28
|
+
* value whichever side refused.
|
|
29
|
+
*/
|
|
30
|
+
const to_local_invalid_params_data = (input, issues) => {
|
|
31
|
+
const reason = resolve_issues_invalid_reason(input, issues);
|
|
32
|
+
return reason === undefined
|
|
33
|
+
? { validation_errors: issues }
|
|
34
|
+
: { validation_errors: issues, reason };
|
|
35
|
+
};
|
|
23
36
|
/**
|
|
24
37
|
* Action event that manages the lifecycle of an action through its state machine.
|
|
25
38
|
*/
|
|
@@ -136,7 +149,7 @@ export class ActionEvent {
|
|
|
136
149
|
// Handler errors (network, server, business logic) DO transition to error phases.
|
|
137
150
|
this.#fail(
|
|
138
151
|
// no need to protect this info
|
|
139
|
-
jsonrpc_error_messages.invalid_params(`failed to parse input: ${z.prettifyError(parsed.error)}`,
|
|
152
|
+
jsonrpc_error_messages.invalid_params(`failed to parse input: ${z.prettifyError(parsed.error)}`, to_local_invalid_params_data(this.spec.input, parsed.error.issues)));
|
|
140
153
|
}
|
|
141
154
|
return this;
|
|
142
155
|
}
|
|
@@ -31,7 +31,7 @@ export interface ActionEventEnvironment {
|
|
|
31
31
|
* notifications and requests. Unset falls back to `console_action_log`;
|
|
32
32
|
* `null` silences them.
|
|
33
33
|
*/
|
|
34
|
-
readonly log?:
|
|
34
|
+
readonly log?: ActionLog | null;
|
|
35
35
|
}
|
|
36
36
|
/** The logging surface the action dispatch path uses. */
|
|
37
37
|
export type ActionLog = Pick<Logger, 'error' | 'warn' | 'debug'>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_event_types.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_event_types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEtF,eAAO,MAAM,cAAc;;;EAAkC,CAAC;AAC9D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,eAAO,MAAM,eAAe;;;;;;EAAiE,CAAC;AAC9F,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAS9D,eAAO,MAAM,6BAA6B,EAAE,MAAM,CACjD,eAAe,EACf,aAAa,CAAC,eAAe,CAAC,CAO9B,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAW1F,CAAC;AAEF,eAAO,MAAM,8BAA8B,EAAE,MAAM,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAU5F,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,qBAAqB,EAAE,CACtB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,gBAAgB,KACnB,CAAC,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC,GAAG,SAAS,CAAC;IACvC,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IACpE;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"action_event_types.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_event_types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEtF,eAAO,MAAM,cAAc;;;EAAkC,CAAC;AAC9D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,eAAO,MAAM,eAAe;;;;;;EAAiE,CAAC;AAC9F,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAS9D,eAAO,MAAM,6BAA6B,EAAE,MAAM,CACjD,eAAe,EACf,aAAa,CAAC,eAAe,CAAC,CAO9B,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAW1F,CAAC;AAEF,eAAO,MAAM,8BAA8B,EAAE,MAAM,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAU5F,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,qBAAqB,EAAE,CACtB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,gBAAgB,KACnB,CAAC,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC,GAAG,SAAS,CAAC;IACvC,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IACpE;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CAChC;AAED,yDAAyD;AACzD,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAEjE;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAIhC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,aAAa,sBAAsB,KAAG,SAAS,GAAG,IAChB,CAAC"}
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
* sits behind the authority gates by choice rather than necessity: a
|
|
29
29
|
* caller those gates refuse is answered 403, not 429.
|
|
30
30
|
* 5. **Validate params (400)** — `spec.input.safeParse(raw_params)` with
|
|
31
|
-
* `z.void()` / `?? {}` rules (`to_input_params`).
|
|
31
|
+
* `z.void()` / `?? {}` rules (`to_input_params`). A refusal is the generic
|
|
32
|
+
* `invalid params`, with `data.reason` when the first issue's field
|
|
33
|
+
* declares an `invalid_reason` (`to_invalid_params_error`) and the Zod
|
|
34
|
+
* issues under DEV only. Runs **after** the authority gates: a
|
|
32
35
|
* 400 describing the action's input shape to a caller those gates
|
|
33
36
|
* refuse would confirm the method exists and describe how to call it,
|
|
34
37
|
* to a channel that should have learned nothing. Coarse authority
|
|
@@ -55,6 +58,7 @@
|
|
|
55
58
|
* @module
|
|
56
59
|
*/
|
|
57
60
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
61
|
+
import type { z } from 'zod';
|
|
58
62
|
import type { Uuid } from '../primitive_schemas.js';
|
|
59
63
|
import { type RequestContext } from '../auth/request_context.js';
|
|
60
64
|
import { type CredentialType } from '../hono_context.js';
|
|
@@ -163,6 +167,18 @@ export type PerformActionResult = {
|
|
|
163
167
|
* the harness omits `account_id`.
|
|
164
168
|
*/
|
|
165
169
|
export declare const perform_action: (input: PerformActionInput, deps: PerformActionDeps) => Promise<PerformActionResult>;
|
|
170
|
+
/**
|
|
171
|
+
* The `invalid_params` error an input validation failure answers: the generic
|
|
172
|
+
* `invalid params` message, `data.reason` when the field the first issue lands
|
|
173
|
+
* on declares an `invalid_reason` and no issue sits on the input itself
|
|
174
|
+
* (`resolve_issues_invalid_reason`), and the Zod issues under DEV only
|
|
175
|
+
* (`dev_only`). With no declared reason, production sends no `data` at all.
|
|
176
|
+
*
|
|
177
|
+
* @param input - the action's input schema
|
|
178
|
+
* @param issues - the failed parse's issues, in Zod's order
|
|
179
|
+
* @returns the JSON-RPC error object
|
|
180
|
+
*/
|
|
181
|
+
export declare const to_invalid_params_error: (input: z.ZodType, issues: ReadonlyArray<z.core.$ZodIssue>) => JsonrpcErrorObject;
|
|
166
182
|
/**
|
|
167
183
|
* The credential-type 403 — one builder, so the dispatcher's gate below and a
|
|
168
184
|
* handler that gates a credential channel on its input (`account_delete`'s
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAKtC,OAAO,EAEN,KAAK,gBAAgB,EAErB,KAAK,kBAAkB,EACvB,MAAM,oBAAoB,CAAC;AAc5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,KAAK,EAAgC,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAGN,KAAK,UAAU,EACf,MAAM,wBAAwB,CAAC;AAEhC;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,MAAM,EAAE,SAAS,CAAC;IAClB,mGAAmG;IACnG,UAAU,EAAE,OAAO,CAAC;IACpB,sDAAsD;IACtD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,yDAAyD;IACzD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,uEAAuE;IACvE,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC;;;;;OAKG;IACH,WAAW,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,MAAM,EAAE,WAAW,CAAC;IACpB,sFAAsF;IACtF,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,uDAAuD;IACvD,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC;IAC/B;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAE,eAAe,EAAE,cAAc,GAAG,IAAI,CAAA;KAAE,CAAC;CACpD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IACjC,gGAAgG;IAChG,EAAE,EAAE,EAAE,CAAC;IACP;;;OAGG;IACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC;;;OAGG;IACH,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3C,uEAAuE;IACvE,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;CAChD;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAC9B;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhG;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAC1B,OAAO,kBAAkB,EACzB,MAAM,iBAAiB,KACrB,OAAO,CAAC,mBAAmB,CAoL7B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,GACnC,OAAO,CAAC,CAAC,OAAO,EAChB,QAAQ,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KACrC,kBAOF,CAAC;AA6DF;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,GAC1C,2BAA2B,aAAa,CAAC,MAAM,CAAC,KAC9C,kBAIA,CAAC;AAyDJ;;;GAGG;AACH,eAAO,MAAM,iCAAiC,GAC7C,IAAI,gBAAgB,EACpB,QAAQ,mBAAmB,KACzB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,gBAAgB,CAAA;CAAE,GAAG,CAC9C;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,kBAAkB,CAAA;CAAE,CAMnD,CAAC"}
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
* sits behind the authority gates by choice rather than necessity: a
|
|
29
29
|
* caller those gates refuse is answered 403, not 429.
|
|
30
30
|
* 5. **Validate params (400)** — `spec.input.safeParse(raw_params)` with
|
|
31
|
-
* `z.void()` / `?? {}` rules (`to_input_params`).
|
|
31
|
+
* `z.void()` / `?? {}` rules (`to_input_params`). A refusal is the generic
|
|
32
|
+
* `invalid params`, with `data.reason` when the first issue's field
|
|
33
|
+
* declares an `invalid_reason` (`to_invalid_params_error`) and the Zod
|
|
34
|
+
* issues under DEV only. Runs **after** the authority gates: a
|
|
32
35
|
* 400 describing the action's input shape to a caller those gates
|
|
33
36
|
* refuse would confirm the method exists and describe how to call it,
|
|
34
37
|
* to a channel that should have learned nothing. Coarse authority
|
|
@@ -57,10 +60,11 @@
|
|
|
57
60
|
import { DEV } from 'esm-env';
|
|
58
61
|
import { apply_authorization_phase, has_any_scoped_role } from '../auth/request_context.js';
|
|
59
62
|
import {} from '../hono_context.js';
|
|
63
|
+
import { resolve_issues_invalid_reason } from '../schema_meta.js';
|
|
60
64
|
import { to_input_params } from '../http/schema_helpers.js';
|
|
61
65
|
import { rate_limit_ip_key } from '../http/ip_canonical.js';
|
|
62
66
|
import { dispatch_with_post_commit_rollback } from '../http/pending_effects.js';
|
|
63
|
-
import { JSONRPC_VERSION } from '../http/jsonrpc.js';
|
|
67
|
+
import { JSONRPC_VERSION, is_jsonrpc_error_code } from '../http/jsonrpc.js';
|
|
64
68
|
import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, http_status_to_jsonrpc_error_code, JSONRPC_ERROR_CODES, dev_only } from '../http/jsonrpc_errors.js';
|
|
65
69
|
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_TOKEN_SCOPE_REQUIRED } from '../http/error_schemas.js';
|
|
66
70
|
import { is_public_auth, needs_actor, parse_acting } from '../http/auth_shape.js';
|
|
@@ -153,7 +157,7 @@ export const perform_action = async (input, deps) => {
|
|
|
153
157
|
const params = to_input_params(spec.input, raw_params);
|
|
154
158
|
const parse_result = spec.input.safeParse(params);
|
|
155
159
|
if (!parse_result.success) {
|
|
156
|
-
return error_result(
|
|
160
|
+
return error_result(to_invalid_params_error(spec.input, parse_result.error.issues));
|
|
157
161
|
}
|
|
158
162
|
const validated_input = parse_result.data;
|
|
159
163
|
// step 6: dispatch — transaction for mutations, pool for reads.
|
|
@@ -193,11 +197,14 @@ export const perform_action = async (input, deps) => {
|
|
|
193
197
|
return await dispatch_with_post_commit_rollback(post_commit_effects, () => use_transaction ? db.transaction((tx) => execute(tx)) : execute(db));
|
|
194
198
|
}
|
|
195
199
|
catch (err) {
|
|
196
|
-
// Duck-type check: Error
|
|
197
|
-
// Avoids cross-realm `instanceof` misses when consumers
|
|
198
|
-
// `ThrownJsonrpcError` (structurally identical, different
|
|
200
|
+
// Duck-type check: an Error whose `code` is a JSON-RPC error code signals
|
|
201
|
+
// a JSON-RPC error. Avoids cross-realm `instanceof` misses when consumers
|
|
202
|
+
// throw their own `ThrownJsonrpcError` (structurally identical, different
|
|
203
|
+
// class identity). The code must be a valid JSON-RPC one, not just a
|
|
204
|
+
// number — a `DOMException` (`code` 20, 23, ...) is an internal error,
|
|
205
|
+
// logged and masked like any other.
|
|
199
206
|
const error_like = err;
|
|
200
|
-
if (err instanceof Error &&
|
|
207
|
+
if (err instanceof Error && is_jsonrpc_error_code(error_like.code)) {
|
|
201
208
|
const code = error_like.code;
|
|
202
209
|
const status = jsonrpc_error_code_to_http_status(code);
|
|
203
210
|
const error = { code, message: err.message };
|
|
@@ -212,6 +219,22 @@ export const perform_action = async (input, deps) => {
|
|
|
212
219
|
return error_result(jsonrpc_error_messages.internal_error(dev_only(err instanceof Error ? err.message : undefined)));
|
|
213
220
|
}
|
|
214
221
|
};
|
|
222
|
+
/**
|
|
223
|
+
* The `invalid_params` error an input validation failure answers: the generic
|
|
224
|
+
* `invalid params` message, `data.reason` when the field the first issue lands
|
|
225
|
+
* on declares an `invalid_reason` and no issue sits on the input itself
|
|
226
|
+
* (`resolve_issues_invalid_reason`), and the Zod issues under DEV only
|
|
227
|
+
* (`dev_only`). With no declared reason, production sends no `data` at all.
|
|
228
|
+
*
|
|
229
|
+
* @param input - the action's input schema
|
|
230
|
+
* @param issues - the failed parse's issues, in Zod's order
|
|
231
|
+
* @returns the JSON-RPC error object
|
|
232
|
+
*/
|
|
233
|
+
export const to_invalid_params_error = (input, issues) => {
|
|
234
|
+
const reason = resolve_issues_invalid_reason(input, issues);
|
|
235
|
+
const issues_data = dev_only({ issues });
|
|
236
|
+
return jsonrpc_error_messages.invalid_params('invalid params', reason === undefined ? issues_data : { reason, ...issues_data });
|
|
237
|
+
};
|
|
215
238
|
const error_result = (error) => ({
|
|
216
239
|
kind: 'error',
|
|
217
240
|
error,
|
|
@@ -54,6 +54,12 @@
|
|
|
54
54
|
* admits a revoked credential; `docs/security.md` §Connection Admission says
|
|
55
55
|
* what that rests on.
|
|
56
56
|
*
|
|
57
|
+
* The acting actor the upgrade resolved is re-read beside the credential on
|
|
58
|
+
* every endpoint, gated or not: an actor soft-deleted under its live account
|
|
59
|
+
* keeps the account's sessions and tokens and its own grant rows, so neither
|
|
60
|
+
* the credential nor the role re-read would refuse it. A tombstoned acting
|
|
61
|
+
* actor is refused like a dead credential.
|
|
62
|
+
*
|
|
57
63
|
* An endpoint gated on `required_roles` re-reads them beside the credential:
|
|
58
64
|
* its upgrade passed the role gate before the socket registered, so a role
|
|
59
65
|
* revocation landing in between would close nothing (`close_sockets_for_role`
|
|
@@ -62,9 +68,9 @@
|
|
|
62
68
|
* admits on a global grant of any listed role, as the gate did.
|
|
63
69
|
*
|
|
64
70
|
* A refused upgrade is closed with `WS_CLOSE_SESSION_REVOKED` (4001, the
|
|
65
|
-
* reason a revoked socket gets) for a dead credential,
|
|
71
|
+
* reason a revoked socket gets) for a dead credential or a deleted acting actor,
|
|
66
72
|
* `WS_CLOSE_PERMISSION_REVOKED` (4006) for a lost role, or
|
|
67
|
-
* `WS_CLOSE_INTERNAL_ERROR` (1011) when
|
|
73
|
+
* `WS_CLOSE_INTERNAL_ERROR` (1011) when a re-read itself failed — fail
|
|
68
74
|
* closed, never admitted unchecked. The handshake has already answered `101`,
|
|
69
75
|
* so a refusal can only be a close frame.
|
|
70
76
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"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"}
|
|
@@ -54,6 +54,12 @@
|
|
|
54
54
|
* admits a revoked credential; `docs/security.md` §Connection Admission says
|
|
55
55
|
* what that rests on.
|
|
56
56
|
*
|
|
57
|
+
* The acting actor the upgrade resolved is re-read beside the credential on
|
|
58
|
+
* every endpoint, gated or not: an actor soft-deleted under its live account
|
|
59
|
+
* keeps the account's sessions and tokens and its own grant rows, so neither
|
|
60
|
+
* the credential nor the role re-read would refuse it. A tombstoned acting
|
|
61
|
+
* actor is refused like a dead credential.
|
|
62
|
+
*
|
|
57
63
|
* An endpoint gated on `required_roles` re-reads them beside the credential:
|
|
58
64
|
* its upgrade passed the role gate before the socket registered, so a role
|
|
59
65
|
* revocation landing in between would close nothing (`close_sockets_for_role`
|
|
@@ -62,9 +68,9 @@
|
|
|
62
68
|
* admits on a global grant of any listed role, as the gate did.
|
|
63
69
|
*
|
|
64
70
|
* A refused upgrade is closed with `WS_CLOSE_SESSION_REVOKED` (4001, the
|
|
65
|
-
* reason a revoked socket gets) for a dead credential,
|
|
71
|
+
* reason a revoked socket gets) for a dead credential or a deleted acting actor,
|
|
66
72
|
* `WS_CLOSE_PERMISSION_REVOKED` (4006) for a lost role, or
|
|
67
|
-
* `WS_CLOSE_INTERNAL_ERROR` (1011) when
|
|
73
|
+
* `WS_CLOSE_INTERNAL_ERROR` (1011) when a re-read itself failed — fail
|
|
68
74
|
* closed, never admitted unchecked. The handshake has already answered `101`,
|
|
69
75
|
* so a refusal can only be a close frame.
|
|
70
76
|
*
|
|
@@ -116,6 +122,7 @@ import { wait } from '@fuzdev/fuz_util/async.ts';
|
|
|
116
122
|
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
117
123
|
import { get_request_context, has_any_scoped_role, refresh_role_grants, require_request_context, token_scope_surface_denial } from '../auth/request_context.js';
|
|
118
124
|
import { get_resolved_auth, revalidate_resolved_auth } from '../auth/resolved_auth.js';
|
|
125
|
+
import { query_active_actors_by_account } from '../auth/account_queries.js';
|
|
119
126
|
import { schedule_credential_deadline } from '../auth/credential_deadline.js';
|
|
120
127
|
import { get_client_ip } from '../http/client_ip.js';
|
|
121
128
|
import { flush_pending_effects, flush_post_commit_effects } from '../http/pending_effects.js';
|
|
@@ -562,14 +569,22 @@ export const register_action_ws = (options) => {
|
|
|
562
569
|
}
|
|
563
570
|
if (!upgrade_preset) {
|
|
564
571
|
let credential_is_live;
|
|
572
|
+
let actor_is_active = true;
|
|
565
573
|
let role_is_held = true;
|
|
566
574
|
try {
|
|
567
575
|
// pool-level `db`: the statements must see every revocation
|
|
568
576
|
// committed before they start
|
|
569
577
|
credential_is_live = await revalidate_resolved_auth({ db }, resolved);
|
|
578
|
+
// The acting actor the upgrade resolved, on every endpoint: a
|
|
579
|
+
// soft-deleted actor keeps its account's credential and its
|
|
580
|
+
// grant rows, so neither re-read above or below refuses it.
|
|
581
|
+
const acting_actor = upgrade_context.actor;
|
|
582
|
+
if (credential_is_live && acting_actor) {
|
|
583
|
+
actor_is_active = (await query_active_actors_by_account({ db }, account_id)).some((a) => a.id === acting_actor.id);
|
|
584
|
+
}
|
|
570
585
|
// Global grants only, like the gate in front: a scoped grant
|
|
571
586
|
// never admitted this socket.
|
|
572
|
-
if (credential_is_live && required_roles.length > 0) {
|
|
587
|
+
if (credential_is_live && actor_is_active && required_roles.length > 0) {
|
|
573
588
|
role_is_held = has_any_scoped_role(await refresh_role_grants(upgrade_context, { db }), required_roles, null);
|
|
574
589
|
}
|
|
575
590
|
}
|
|
@@ -581,7 +596,7 @@ export const register_action_ws = (options) => {
|
|
|
581
596
|
log.info('ws upgrade admission: re-check ended by the shutdown', error);
|
|
582
597
|
}
|
|
583
598
|
else {
|
|
584
|
-
log.error('ws upgrade admission: credential or role re-check failed', error);
|
|
599
|
+
log.error('ws upgrade admission: credential, actor, or role re-check failed', error);
|
|
585
600
|
}
|
|
586
601
|
refuse_upgrade(ws, WS_CLOSE_INTERNAL_ERROR, 'internal error');
|
|
587
602
|
return null;
|
|
@@ -591,6 +606,11 @@ export const register_action_ws = (options) => {
|
|
|
591
606
|
refuse_upgrade(ws, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON);
|
|
592
607
|
return null;
|
|
593
608
|
}
|
|
609
|
+
if (!actor_is_active) {
|
|
610
|
+
log.info('ws upgrade admission: acting actor deleted during the upgrade', connection_id, account_id);
|
|
611
|
+
refuse_upgrade(ws, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
594
614
|
if (!role_is_held) {
|
|
595
615
|
log.info('ws upgrade admission: role revoked during the upgrade', connection_id, account_id);
|
|
596
616
|
refuse_upgrade(ws, WS_CLOSE_PERMISSION_REVOKED, WS_CLOSE_PERMISSION_REVOKED_REASON);
|
|
@@ -77,10 +77,12 @@ export type AuditEventHandler = (event: AuditLogEvent) => void;
|
|
|
77
77
|
* - `none` → nothing — every other event, and any event type fuz_app does not
|
|
78
78
|
* define
|
|
79
79
|
*
|
|
80
|
-
*
|
|
81
|
-
* identifiers (e.g. a `session_revoke` that the DB
|
|
82
|
-
* the submitted session_id), so reacting to
|
|
83
|
-
* user close another user's socket by guessing a
|
|
80
|
+
* Acts only on `outcome === 'success'` events — a failure carries
|
|
81
|
+
* attacker-controlled identifiers (e.g. a `session_revoke` that the DB
|
|
82
|
+
* rejected still records the submitted session_id), so reacting to it would
|
|
83
|
+
* let any authenticated user close another user's socket by guessing a
|
|
84
|
+
* session hash or token id. Matching the success rather than skipping the
|
|
85
|
+
* failure keeps an outcome outside the enum inert, as on the Rust spine.
|
|
84
86
|
*
|
|
85
87
|
* @param closer - what to close on — a `BackendWebsocketTransport`, or a
|
|
86
88
|
* `RealtimeCloser` to reach every transport from one listener
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAE/D
|
|
1
|
+
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,gBAAgB,EAAE,KAAK,MAAM,KAAG,iBA6D5E,CAAC"}
|
|
@@ -66,10 +66,12 @@ import { to_global_role_revocation, to_revocation_scope } from '../auth/audit_lo
|
|
|
66
66
|
* - `none` → nothing — every other event, and any event type fuz_app does not
|
|
67
67
|
* define
|
|
68
68
|
*
|
|
69
|
-
*
|
|
70
|
-
* identifiers (e.g. a `session_revoke` that the DB
|
|
71
|
-
* the submitted session_id), so reacting to
|
|
72
|
-
* user close another user's socket by guessing a
|
|
69
|
+
* Acts only on `outcome === 'success'` events — a failure carries
|
|
70
|
+
* attacker-controlled identifiers (e.g. a `session_revoke` that the DB
|
|
71
|
+
* rejected still records the submitted session_id), so reacting to it would
|
|
72
|
+
* let any authenticated user close another user's socket by guessing a
|
|
73
|
+
* session hash or token id. Matching the success rather than skipping the
|
|
74
|
+
* failure keeps an outcome outside the enum inert, as on the Rust spine.
|
|
73
75
|
*
|
|
74
76
|
* @param closer - what to close on — a `BackendWebsocketTransport`, or a
|
|
75
77
|
* `RealtimeCloser` to reach every transport from one listener
|
|
@@ -81,8 +83,9 @@ import { to_global_role_revocation, to_revocation_scope } from '../auth/audit_lo
|
|
|
81
83
|
*/
|
|
82
84
|
export const create_ws_auth_guard = (closer, log) => {
|
|
83
85
|
return (event) => {
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
+
// only a committed revocation closes — failed mutations carry
|
|
87
|
+
// attacker-controlled metadata, and anything else is not a revocation
|
|
88
|
+
if (event.outcome !== 'success')
|
|
86
89
|
return;
|
|
87
90
|
const scope = to_revocation_scope(event.event_type);
|
|
88
91
|
switch (scope) {
|
|
@@ -35,7 +35,7 @@ export interface WsEndpointSpec {
|
|
|
35
35
|
* `AppServerOptions.allowed_origins`, the list the HTTP routes check.
|
|
36
36
|
* Set it only to narrow one endpoint: the auth middleware's origin check
|
|
37
37
|
* runs on the upgrade too, so a request reaches the endpoint only when
|
|
38
|
-
* its origin passes both lists.
|
|
38
|
+
* its origin passes both lists. An empty list throws at assembly.
|
|
39
39
|
*/
|
|
40
40
|
allowed_origins?: ReadonlyArray<RegExp>;
|
|
41
41
|
/**
|