@fuzdev/fuz_app 0.110.1 → 0.112.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 +15 -11
- package/dist/actions/action_rpc.d.ts +3 -3
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +4 -6
- package/dist/actions/peer_ping.d.ts +8 -0
- package/dist/actions/peer_ping.d.ts.map +1 -1
- package/dist/actions/peer_ping.js +8 -0
- package/dist/actions/peer_request.d.ts +1 -1
- package/dist/actions/peer_request.js +1 -1
- package/dist/actions/perform_action.d.ts +16 -9
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +30 -21
- package/dist/actions/register_action_ws.d.ts +1 -1
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +88 -21
- package/dist/auth/CLAUDE.md +59 -10
- package/dist/auth/account_action_specs.d.ts +0 -2
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +10 -1
- package/dist/auth/account_queries.d.ts +22 -0
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +70 -16
- package/dist/auth/account_schema.d.ts +27 -9
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +23 -3
- package/dist/auth/actor_search_actions.d.ts +3 -3
- package/dist/auth/actor_search_actions.d.ts.map +1 -1
- package/dist/auth/actor_search_actions.js +10 -6
- package/dist/auth/admin_action_specs.d.ts +0 -2
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/api_token_queries.d.ts +9 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +36 -8
- package/dist/auth/app_settings_queries.d.ts +8 -0
- package/dist/auth/app_settings_queries.d.ts.map +1 -1
- package/dist/auth/app_settings_queries.js +16 -3
- package/dist/auth/audit_log_queries.d.ts +21 -3
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +38 -7
- package/dist/auth/audit_log_schema.d.ts +0 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/auth_ddl.d.ts +6 -0
- package/dist/auth/auth_ddl.d.ts.map +1 -1
- package/dist/auth/auth_ddl.js +9 -0
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +3 -8
- package/dist/auth/cell_audit_actions.d.ts.map +1 -1
- package/dist/auth/cell_audit_actions.js +1 -1
- package/dist/auth/cell_field_actions.js +1 -1
- package/dist/auth/cell_grant_actions.js +1 -1
- package/dist/auth/cell_item_actions.js +1 -1
- package/dist/auth/invite_queries.d.ts +10 -0
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +28 -7
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +10 -0
- package/dist/auth/role_grant_offer_ddl.d.ts +34 -2
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +61 -2
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +48 -53
- package/dist/auth/role_grant_queries.d.ts +18 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +58 -18
- package/dist/auth/session_queries.d.ts +12 -3
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +26 -6
- package/dist/db/CLAUDE.md +34 -0
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +3 -1
- package/dist/db/cell_field_queries.d.ts +12 -3
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +25 -7
- package/dist/db/cell_grant_queries.d.ts +10 -2
- package/dist/db/cell_grant_queries.d.ts.map +1 -1
- package/dist/db/cell_grant_queries.js +29 -11
- package/dist/db/cell_item_queries.d.ts +13 -2
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +27 -7
- package/dist/db/cell_queries.d.ts +32 -3
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +57 -22
- package/dist/db/fact_queries.d.ts +7 -0
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +27 -4
- package/dist/db/schema_ready.d.ts +6 -1
- package/dist/db/schema_ready.d.ts.map +1 -1
- package/dist/db/schema_ready.js +6 -2
- package/dist/db/sql_columns.d.ts +144 -0
- package/dist/db/sql_columns.d.ts.map +1 -0
- package/dist/db/sql_columns.js +164 -0
- package/dist/db/sql_identifier.d.ts +6 -5
- package/dist/db/sql_identifier.d.ts.map +1 -1
- package/dist/db/sql_identifier.js +6 -5
- package/dist/http/CLAUDE.md +4 -2
- package/dist/http/jsonrpc_helpers.d.ts +13 -0
- package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
- package/dist/http/jsonrpc_helpers.js +13 -0
- package/dist/http/route_spec.d.ts +2 -2
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +1 -2
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +7 -2
- package/dist/testing/CLAUDE.md +55 -28
- package/dist/testing/admin_integration.d.ts +0 -3
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +0 -1
- package/dist/testing/adversarial_404.d.ts.map +1 -1
- package/dist/testing/adversarial_404.js +4 -1
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +5 -4
- package/dist/testing/app_server.d.ts +4 -3
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -5
- package/dist/testing/attack_surface.d.ts +16 -0
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +36 -13
- package/dist/testing/audit_completeness.d.ts +0 -3
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +0 -1
- package/dist/testing/auth_apps.d.ts +9 -2
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +9 -2
- package/dist/testing/cross_backend/account_lifecycle.d.ts +5 -4
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +7 -0
- package/dist/testing/cross_backend/action_manifest.d.ts +19 -3
- package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest.js +10 -4
- package/dist/testing/cross_backend/action_manifest_parity.d.ts +9 -3
- package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest_parity.js +6 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts +3 -2
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +4 -2
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +1 -1
- package/dist/testing/cross_backend/cell_crud.d.ts +2 -2
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +10 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts +3 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.d.ts +2 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts +2 -2
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/expected_schema.json +1 -1
- package/dist/testing/cross_backend/fact_serving.d.ts +2 -2
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts +17 -2
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.js +23 -3
- package/dist/testing/cross_backend/origin.d.ts +4 -2
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +11 -7
- package/dist/testing/cross_backend/query_shape.d.ts +6 -5
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/ready.d.ts +2 -2
- package/dist/testing/cross_backend/ready.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.js +7 -1
- package/dist/testing/cross_backend/setup.d.ts +26 -6
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.d.ts +5 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +1 -5
- package/dist/testing/cross_backend/testing_backdoor.js +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +6 -0
- package/dist/testing/cross_backend/token_scope_surface.d.ts +2 -2
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/wire_shapes.d.ts +22 -0
- package/dist/testing/cross_backend/wire_shapes.d.ts.map +1 -0
- package/dist/testing/cross_backend/wire_shapes.js +41 -0
- package/dist/testing/data_exposure.d.ts +0 -3
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +0 -1
- package/dist/testing/db.d.ts +62 -7
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +86 -28
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -2
- package/dist/testing/round_trip.d.ts +0 -3
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +0 -4
- package/dist/testing/rpc_attack_surface.js +2 -2
- package/dist/testing/rpc_round_trip.d.ts +0 -3
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +0 -1
- package/dist/testing/schema_generators.d.ts +15 -0
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +45 -11
- package/dist/testing/standard.d.ts +5 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +2 -7
- package/dist/timestamp.d.ts +58 -0
- package/dist/timestamp.d.ts.map +1 -0
- package/dist/timestamp.js +57 -0
- package/dist/ui/AdminOverview.svelte +5 -10
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/RoleGrantOfferHistory.svelte +3 -1
- package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +6 -2
- package/package.json +1 -1
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -52,8 +52,9 @@ Optional fields:
|
|
|
52
52
|
- `rate_limit?: 'ip' | 'account' | 'both'` — opts the action into the
|
|
53
53
|
dispatcher's per-action rate-limit hook. **Throttle-requests semantics**
|
|
54
54
|
— every invocation records regardless of outcome (different from REST
|
|
55
|
-
login's throttle-failures)
|
|
56
|
-
|
|
55
|
+
login's throttle-failures), and the gate fires ahead of param validation
|
|
56
|
+
so malformed calls are charged too. `'account'` / `'both'` rejected at
|
|
57
|
+
registration when paired with `auth.account !== 'required'`. Limiters configured via
|
|
57
58
|
`AppServerOptions.action_ip_rate_limiter` / `action_account_rate_limiter`
|
|
58
59
|
and threaded into both dispatchers automatically.
|
|
59
60
|
|
|
@@ -210,10 +211,13 @@ The HTTP RPC dispatcher is a thin shim around `perform_action`
|
|
|
210
211
|
parsing, GET vs POST split, `c.json` binding); the
|
|
211
212
|
auth/validation/dispatch pipeline is shared with the WebSocket dispatcher.
|
|
212
213
|
|
|
213
|
-
**Phase order: 401 → authz → 403 → 400 → handler.** Authorize first,
|
|
214
|
-
validate after — a caller the authority gates refuse never learns
|
|
215
|
-
action's input shape,
|
|
216
|
-
ones
|
|
214
|
+
**Phase order: 401 → authz → 403 → 429 → 400 → handler.** Authorize first,
|
|
215
|
+
throttle, validate after — a caller the authority gates refuse never learns
|
|
216
|
+
the action's input shape, the coarse authority facts settle before the fine
|
|
217
|
+
ones, and a malformed flood burns the budget it should. The Rust spine's
|
|
218
|
+
dispatcher enforces the same order: it validates the `acting` selector at
|
|
219
|
+
the dispatcher after the rate limit and leaves every other field to the
|
|
220
|
+
handlers.
|
|
217
221
|
|
|
218
222
|
Shim responsibilities (per-request):
|
|
219
223
|
|
|
@@ -286,13 +290,13 @@ The transport-agnostic post-parse pipeline. Each transport assembles a
|
|
|
286
290
|
`perform_action(input, deps)`, and binds the discriminated
|
|
287
291
|
`PerformActionResult` to its wire shape.
|
|
288
292
|
|
|
289
|
-
Pipeline (401 → authz → 403 → 400 → handler):
|
|
293
|
+
Pipeline (401 → authz → 403 → 429 → 400 → handler):
|
|
290
294
|
|
|
291
295
|
1. Pre-authorization auth (401)
|
|
292
|
-
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
|
|
296
|
+
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`
|
|
293
297
|
3. Post-authorization auth (403) — credential-type gate, then token scope, then role
|
|
294
|
-
4.
|
|
295
|
-
5.
|
|
298
|
+
4. Rate limit (429) — throttle-requests semantics, ahead of validation so malformed params charge the budget and a throttled caller pays no schema work
|
|
299
|
+
5. Validate params (400) — `spec.input.safeParse` with `z.void()` / `?? {}` rules
|
|
296
300
|
6. Dispatch + DEV output validation + error normalization — `spec.side_effects` picks transaction vs pool. `ThrownJsonrpcError` preserves code + data; generic throws become `internal_error`
|
|
297
301
|
|
|
298
302
|
`PerformActionInput` carries `account_id`, `credential_type`, `client_ip`,
|
|
@@ -529,7 +533,7 @@ dispatcher without the origin/auth front-stack.
|
|
|
529
533
|
Per-message dispatch delegates to `perform_action` — the shared core that
|
|
530
534
|
HTTP RPC also calls. `register_action_ws` owns only WS-specific concerns:
|
|
531
535
|
|
|
532
|
-
- **Wire envelope parsing** — JSON.parse → batch rejection → notification interception (cancel, silent drop) → per-message dispatch
|
|
536
|
+
- **Wire envelope parsing** — JSON.parse → batch rejection → peer-response routing → envelope-version guard → notification interception (cancel, silent drop) → `JsonrpcRequest.safeParse` → per-message dispatch. The version guard is what makes `{method: 'x'}` and `{jsonrpc: '1.0', method: 'x'}` an `invalid_request` answer rather than a silently dropped notification, matching the first check the Rust `classify` runs. Peer-response routing is the twin's response shape exactly — no `method` key, a string/number `id`, exactly one of `result` / `error` — so a hybrid frame like `{method, id, result}` stays a dispatchable request instead of being demuxed and swallowed. The envelope schema is the same one the HTTP POST path runs, so `params: null`, an array `params`, or a non-string/number `id` answers `invalid_request` (-32600) on both transports rather than reaching the dispatcher as `invalid_params`. It runs **after** the notification/cancel block: notifications carry no id to echo and stay unvalidated, and `cancel` routing precedes it. The version guard echoes the id via `to_jsonrpc_envelope_id` (a scalar frame answers `id: null`), the later steps via `to_jsonrpc_message_id`.
|
|
533
537
|
- **Cancel-notification interception** — `{request_id → AbortController}` map; aborts the matching pending controller before the cancel bubbles past the dispatcher
|
|
534
538
|
- **Socket-scoped notify** — `(method, params) => ws.send(notification)`, threaded into `perform_action` as `notify`
|
|
535
539
|
- **Composed abort signal** — `AbortSignal.any([socket_close, per_request_cancel])`, threaded as `signal`
|
|
@@ -61,8 +61,8 @@ export interface ActionContext {
|
|
|
61
61
|
db: Db;
|
|
62
62
|
/**
|
|
63
63
|
* Eager fire-and-forget queue — push the in-flight `Promise<void>` for
|
|
64
|
-
* pool writes already running (audit emits,
|
|
65
|
-
*
|
|
64
|
+
* pool writes already running (audit emits, api-token usage
|
|
65
|
+
* tracking). Drained via `flush_pending_effects` after the
|
|
66
66
|
* handler returns.
|
|
67
67
|
*/
|
|
68
68
|
pending_effects: Array<Promise<void>>;
|
|
@@ -85,7 +85,7 @@ export interface ActionContext {
|
|
|
85
85
|
/**
|
|
86
86
|
* Credential channel the request arrived on (`'session'` | `'api_token'` |
|
|
87
87
|
* `'daemon_token'`), or `null` for anonymous requests. Same value the
|
|
88
|
-
* dispatcher's `credential_types` gate consumed at step
|
|
88
|
+
* dispatcher's `credential_types` gate consumed at step 3 — exposed here
|
|
89
89
|
* so handlers can record it in audit metadata (defense in depth: the
|
|
90
90
|
* gate may be loosened or bypassed in a future refactor, but the audit
|
|
91
91
|
* row preserves what actually authenticated the request).
|
|
@@ -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,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE1E,OAAO,EAEN,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAGN,KAAK,gBAAgB,EAErB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,6 +20,7 @@ import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY, TEST_CONTEXT_PRES
|
|
|
20
20
|
import { compile_action_registry } from './compile_action_registry.js';
|
|
21
21
|
import { JSONRPC_VERSION, JsonrpcRequest } from '../http/jsonrpc.js';
|
|
22
22
|
import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, JSONRPC_ERROR_CODES, dev_only } from '../http/jsonrpc_errors.js';
|
|
23
|
+
import { to_jsonrpc_envelope_id } from '../http/jsonrpc_helpers.js';
|
|
23
24
|
import { perform_action, perform_action_result_to_envelope } from './perform_action.js';
|
|
24
25
|
/**
|
|
25
26
|
* Pair a spec with a handler while preserving per-method input/output types
|
|
@@ -200,12 +201,9 @@ export const create_rpc_endpoint = (options) => {
|
|
|
200
201
|
}
|
|
201
202
|
const envelope = JsonrpcRequest.safeParse(body);
|
|
202
203
|
if (!envelope.success) {
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
: null;
|
|
207
|
-
const id = typeof raw_id === 'string' || typeof raw_id === 'number' ? raw_id : null;
|
|
208
|
-
const error = jsonrpc_error_envelope(id, jsonrpc_error_messages.invalid_request(dev_only({ issues: envelope.error.issues })));
|
|
204
|
+
// echo the id when the body is an object carrying one — the same
|
|
205
|
+
// extraction the WS path runs; a scalar body answers `id: null`
|
|
206
|
+
const error = jsonrpc_error_envelope(to_jsonrpc_envelope_id(body), jsonrpc_error_messages.invalid_request(dev_only({ issues: envelope.error.issues })));
|
|
209
207
|
return c.json(error, 400);
|
|
210
208
|
}
|
|
211
209
|
return dispatch(c, route, envelope.data.method, envelope.data.params, envelope.data.id, false);
|
|
@@ -87,6 +87,13 @@ export type PingResponse = z.infer<typeof PingResponse>;
|
|
|
87
87
|
* client→server invocation drives a server→client request); `auth: public`
|
|
88
88
|
* (liveness is non-sensitive; the upgrade authenticated the socket);
|
|
89
89
|
* `side_effects: false` (no state change — the handler only round-trips a ping).
|
|
90
|
+
*
|
|
91
|
+
* Public is not ungoverned: `rate_limit: 'ip'` is the one throttle an
|
|
92
|
+
* unauthenticated method can be given (there is no account to key on). It
|
|
93
|
+
* bounds anonymous fan-out and keeps the class at parity with the Rust spec's
|
|
94
|
+
* `RateLimitClass::Ip` — there the no-transport refusal costs two pool
|
|
95
|
+
* checkouts per call; here the public auth shape skips the authorization
|
|
96
|
+
* phase, so the refusal is free and the class is carried for the bound alone.
|
|
90
97
|
*/
|
|
91
98
|
export declare const peer_ping_action_spec: {
|
|
92
99
|
method: string;
|
|
@@ -106,6 +113,7 @@ export declare const peer_ping_action_spec: {
|
|
|
106
113
|
protocol_version: z.ZodNumber;
|
|
107
114
|
}, z.core.$strict>;
|
|
108
115
|
async: true;
|
|
116
|
+
rate_limit: "ip";
|
|
109
117
|
description: string;
|
|
110
118
|
};
|
|
111
119
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peer_ping.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/peer_ping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGhE,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAE5C;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAIvC,kDAAkD;AAClD,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,iDAAiD;AACjD,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAClE,iHAAiH;AACjH,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,sEAAsE;AACtE,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AACxE,0EAA0E;AAC1E,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AACxE,qFAAqF;AACrF,eAAO,MAAM,+BAA+B,6BAA6B,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;mBAQf,CAAC;AACd,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB;;kBAE5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;GAGG;AACH,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD
|
|
1
|
+
{"version":3,"file":"peer_ping.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/peer_ping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGhE,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAE5C;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAIvC,kDAAkD;AAClD,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,iDAAiD;AACjD,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAClE,iHAAiH;AACjH,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,sEAAsE;AACtE,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AACxE,0EAA0E;AAC1E,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AACxE,qFAAqF;AACrF,eAAO,MAAM,+BAA+B,6BAA6B,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;mBAQf,CAAC;AACd,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB;;kBAE5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;GAGG;AACH,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;CAaG,CAAC;AA+BtC;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAC7B,OAAO,eAAe,EACtB,KAAK,aAAa,KAChB,OAAO,CAAC,YAAY,CAqCtB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,OAAO,KAAG,YAGrD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAG9B,CAAC"}
|
|
@@ -91,6 +91,13 @@ export const PingResponse = z.strictObject({
|
|
|
91
91
|
* client→server invocation drives a server→client request); `auth: public`
|
|
92
92
|
* (liveness is non-sensitive; the upgrade authenticated the socket);
|
|
93
93
|
* `side_effects: false` (no state change — the handler only round-trips a ping).
|
|
94
|
+
*
|
|
95
|
+
* Public is not ungoverned: `rate_limit: 'ip'` is the one throttle an
|
|
96
|
+
* unauthenticated method can be given (there is no account to key on). It
|
|
97
|
+
* bounds anonymous fan-out and keeps the class at parity with the Rust spec's
|
|
98
|
+
* `RateLimitClass::Ip` — there the no-transport refusal costs two pool
|
|
99
|
+
* checkouts per call; here the public auth shape skips the authorization
|
|
100
|
+
* phase, so the refusal is free and the class is carried for the bound alone.
|
|
94
101
|
*/
|
|
95
102
|
export const peer_ping_action_spec = {
|
|
96
103
|
method: PEER_PING_METHOD,
|
|
@@ -101,6 +108,7 @@ export const peer_ping_action_spec = {
|
|
|
101
108
|
input: PingActionInput,
|
|
102
109
|
output: PingResponse,
|
|
103
110
|
async: true,
|
|
111
|
+
rate_limit: 'ip',
|
|
104
112
|
description: 'Liveness round-trip — the handler pings the originating client back over its socket, ' +
|
|
105
113
|
'validates the echo, and returns it. Refused as no-transport over HTTP RPC.'
|
|
106
114
|
};
|
|
@@ -25,7 +25,7 @@ export declare const DEFAULT_PEER_REQUEST_TIMEOUT = 10000;
|
|
|
25
25
|
/**
|
|
26
26
|
* Per-connection cap on concurrent in-flight server→client requests. A caller
|
|
27
27
|
* past it gets `too_many_in_flight` instead of growing the pending map
|
|
28
|
-
* unbounded. Twin of the Rust spine's `
|
|
28
|
+
* unbounded. Twin of the Rust spine's `DEFAULT_MAX_OUTBOUND_PEER_REQUESTS_PER_CONN`.
|
|
29
29
|
*/
|
|
30
30
|
export declare const MAX_IN_FLIGHT_PEER_REQUESTS_PER_CONNECTION = 256;
|
|
31
31
|
/**
|
|
@@ -23,7 +23,7 @@ export const DEFAULT_PEER_REQUEST_TIMEOUT = 10_000;
|
|
|
23
23
|
/**
|
|
24
24
|
* Per-connection cap on concurrent in-flight server→client requests. A caller
|
|
25
25
|
* past it gets `too_many_in_flight` instead of growing the pending map
|
|
26
|
-
* unbounded. Twin of the Rust spine's `
|
|
26
|
+
* unbounded. Twin of the Rust spine's `DEFAULT_MAX_OUTBOUND_PEER_REQUESTS_PER_CONN`.
|
|
27
27
|
*/
|
|
28
28
|
export const MAX_IN_FLIGHT_PEER_REQUESTS_PER_CONNECTION = 256;
|
|
29
29
|
/**
|
|
@@ -18,7 +18,16 @@
|
|
|
18
18
|
* 3. **Post-authorization auth (403)** — gates `auth.credential_types`,
|
|
19
19
|
* the credential's `token_scope`, and `auth.roles` against the resolved
|
|
20
20
|
* context.
|
|
21
|
-
* 4. **
|
|
21
|
+
* 4. **Rate limit (429)** — per-action IP / account throttling, throttle-
|
|
22
|
+
* requests semantics (every invocation records, regardless of outcome).
|
|
23
|
+
* Ahead of validation so the budget is charged for malformed params too
|
|
24
|
+
* — a flood of garbage bodies must cost the sender its quota, not slip
|
|
25
|
+
* past the limiter on a cheap 400 — and so a throttled caller never pays
|
|
26
|
+
* for schema work. The account key is the credential's own account
|
|
27
|
+
* (`request_context.account.id`), settled by authentication, so the gate
|
|
28
|
+
* sits behind the authority gates by choice rather than necessity: a
|
|
29
|
+
* caller those gates refuse is answered 403, not 429.
|
|
30
|
+
* 5. **Validate params (400)** — `spec.input.safeParse(raw_params)` with
|
|
22
31
|
* `z.void()` / `?? {}` rules. Runs **after** the authority gates: a
|
|
23
32
|
* 400 describing the action's input shape to a caller those gates
|
|
24
33
|
* refuse would confirm the method exists and describe how to call it,
|
|
@@ -26,13 +35,11 @@
|
|
|
26
35
|
* facts before fine ones, and no shape disclosure before authority is
|
|
27
36
|
* settled — the same argument that puts the credential gate ahead of
|
|
28
37
|
* the scope gate. The cost: a malformed request from an *authenticated*
|
|
29
|
-
* caller
|
|
30
|
-
* before being rejected,
|
|
31
|
-
* Not a new DoS lever — the same caller reaches that work anyway
|
|
32
|
-
* sending well-formed params — but
|
|
33
|
-
*
|
|
34
|
-
* 5. **Rate limit (429)** — per-action IP / account throttling, throttle-
|
|
35
|
-
* requests semantics (every invocation records, regardless of outcome).
|
|
38
|
+
* caller pays for the authorization phase's actor + role_grant reads
|
|
39
|
+
* before being rejected, rather than being turned away by validation
|
|
40
|
+
* first. Not a new DoS lever — the same caller reaches that work anyway
|
|
41
|
+
* by sending well-formed params — but there is no cheap-rejection path
|
|
42
|
+
* for buggy clients.
|
|
36
43
|
* 6. **Dispatch + DEV-only output validation + error normalization** —
|
|
37
44
|
* `spec.side_effects` picks transaction (`deps.db.transaction`) vs
|
|
38
45
|
* pool. Handler throws roll back the transaction; the catch sits
|
|
@@ -150,7 +157,7 @@ export type PerformActionResult = {
|
|
|
150
157
|
* transport calls into this with pre-parsed inputs and binds the result
|
|
151
158
|
* to its wire shape.
|
|
152
159
|
*
|
|
153
|
-
* Phase order: 401 → authz → 403 → 400 → handler. On the test-preset path
|
|
160
|
+
* Phase order: 401 → authz → 403 → 429 → 400 → handler. On the test-preset path
|
|
154
161
|
* the dispatcher skips the live authorization phase and uses the supplied
|
|
155
162
|
* pre-baked context for post-authorization checks; the 401 still fires when
|
|
156
163
|
* the harness omits `account_id`.
|
|
@@ -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":"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"}
|
|
@@ -18,7 +18,16 @@
|
|
|
18
18
|
* 3. **Post-authorization auth (403)** — gates `auth.credential_types`,
|
|
19
19
|
* the credential's `token_scope`, and `auth.roles` against the resolved
|
|
20
20
|
* context.
|
|
21
|
-
* 4. **
|
|
21
|
+
* 4. **Rate limit (429)** — per-action IP / account throttling, throttle-
|
|
22
|
+
* requests semantics (every invocation records, regardless of outcome).
|
|
23
|
+
* Ahead of validation so the budget is charged for malformed params too
|
|
24
|
+
* — a flood of garbage bodies must cost the sender its quota, not slip
|
|
25
|
+
* past the limiter on a cheap 400 — and so a throttled caller never pays
|
|
26
|
+
* for schema work. The account key is the credential's own account
|
|
27
|
+
* (`request_context.account.id`), settled by authentication, so the gate
|
|
28
|
+
* sits behind the authority gates by choice rather than necessity: a
|
|
29
|
+
* caller those gates refuse is answered 403, not 429.
|
|
30
|
+
* 5. **Validate params (400)** — `spec.input.safeParse(raw_params)` with
|
|
22
31
|
* `z.void()` / `?? {}` rules. Runs **after** the authority gates: a
|
|
23
32
|
* 400 describing the action's input shape to a caller those gates
|
|
24
33
|
* refuse would confirm the method exists and describe how to call it,
|
|
@@ -26,13 +35,11 @@
|
|
|
26
35
|
* facts before fine ones, and no shape disclosure before authority is
|
|
27
36
|
* settled — the same argument that puts the credential gate ahead of
|
|
28
37
|
* the scope gate. The cost: a malformed request from an *authenticated*
|
|
29
|
-
* caller
|
|
30
|
-
* before being rejected,
|
|
31
|
-
* Not a new DoS lever — the same caller reaches that work anyway
|
|
32
|
-
* sending well-formed params — but
|
|
33
|
-
*
|
|
34
|
-
* 5. **Rate limit (429)** — per-action IP / account throttling, throttle-
|
|
35
|
-
* requests semantics (every invocation records, regardless of outcome).
|
|
38
|
+
* caller pays for the authorization phase's actor + role_grant reads
|
|
39
|
+
* before being rejected, rather than being turned away by validation
|
|
40
|
+
* first. Not a new DoS lever — the same caller reaches that work anyway
|
|
41
|
+
* by sending well-formed params — but there is no cheap-rejection path
|
|
42
|
+
* for buggy clients.
|
|
36
43
|
* 6. **Dispatch + DEV-only output validation + error normalization** —
|
|
37
44
|
* `spec.side_effects` picks transaction (`deps.db.transaction`) vs
|
|
38
45
|
* pool. Handler throws roll back the transaction; the catch sits
|
|
@@ -62,7 +69,7 @@ import { token_scope_admits_method, token_scope_method_capability } from '../aut
|
|
|
62
69
|
* transport calls into this with pre-parsed inputs and binds the result
|
|
63
70
|
* to its wire shape.
|
|
64
71
|
*
|
|
65
|
-
* Phase order: 401 → authz → 403 → 400 → handler. On the test-preset path
|
|
72
|
+
* Phase order: 401 → authz → 403 → 429 → 400 → handler. On the test-preset path
|
|
66
73
|
* the dispatcher skips the live authorization phase and uses the supplied
|
|
67
74
|
* pre-baked context for post-authorization checks; the 401 still fires when
|
|
68
75
|
* the harness omits `account_id`.
|
|
@@ -77,10 +84,10 @@ export const perform_action = async (input, deps) => {
|
|
|
77
84
|
if (pre)
|
|
78
85
|
return error_result(pre);
|
|
79
86
|
// step 2: authorization phase. `acting` reads off the raw params rather
|
|
80
|
-
// than a validated field, because validation
|
|
87
|
+
// than a validated field, because validation runs after the authority
|
|
81
88
|
// gates. Per registry-time invariant 2,
|
|
82
89
|
// `auth.actor !== 'none' ⟺ input declares acting?: ActingActor` — so the
|
|
83
|
-
// selector is present on exactly the specs that need it, and step
|
|
90
|
+
// selector is present on exactly the specs that need it, and step 5 is
|
|
84
91
|
// what rejects a malformed one.
|
|
85
92
|
let request_context = null;
|
|
86
93
|
if (preset !== undefined) {
|
|
@@ -105,18 +112,11 @@ export const perform_action = async (input, deps) => {
|
|
|
105
112
|
const post = check_action_auth_post_authorization(action_auth, request_context, credential_type, action.spec.method, token_scope);
|
|
106
113
|
if (post)
|
|
107
114
|
return error_result(post);
|
|
108
|
-
// step 4:
|
|
109
|
-
// registration sites reject `z.null()` inputs. Empty-body convention
|
|
110
|
-
// (`raw_params ?? {}`) lets all-optional-object methods omit `params`.
|
|
111
|
-
const params = is_void_schema(spec.input) ? raw_params : (raw_params ?? {});
|
|
112
|
-
const parse_result = spec.input.safeParse(params);
|
|
113
|
-
if (!parse_result.success) {
|
|
114
|
-
return error_result(jsonrpc_error_messages.invalid_params('invalid params', dev_only({ issues: parse_result.error.issues })));
|
|
115
|
-
}
|
|
116
|
-
const validated_input = parse_result.data;
|
|
117
|
-
// step 5: rate limit — throttle-requests semantics (record on every
|
|
115
|
+
// step 4: rate limit — throttle-requests semantics (record on every
|
|
118
116
|
// invocation, no success-reset). Same limiters shared with the WS
|
|
119
117
|
// dispatcher so an attacker can't switch transports to bypass the budget.
|
|
118
|
+
// Runs ahead of validation so a flood of malformed params burns the
|
|
119
|
+
// budget it should, and so a throttled caller pays no schema work.
|
|
120
120
|
const rate_limit = spec.rate_limit;
|
|
121
121
|
if (rate_limit) {
|
|
122
122
|
const ip_check = action_ip_rate_limiter && (rate_limit === 'ip' || rate_limit === 'both');
|
|
@@ -141,6 +141,15 @@ export const perform_action = async (input, deps) => {
|
|
|
141
141
|
action_account_rate_limiter.record(account_keyed_context.account.id);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
// step 5: validate params. JSON-RPC 2.0 §4.2 forbids `params: null`;
|
|
145
|
+
// registration sites reject `z.null()` inputs. Empty-body convention
|
|
146
|
+
// (`raw_params ?? {}`) lets all-optional-object methods omit `params`.
|
|
147
|
+
const params = is_void_schema(spec.input) ? raw_params : (raw_params ?? {});
|
|
148
|
+
const parse_result = spec.input.safeParse(params);
|
|
149
|
+
if (!parse_result.success) {
|
|
150
|
+
return error_result(jsonrpc_error_messages.invalid_params('invalid params', dev_only({ issues: parse_result.error.issues })));
|
|
151
|
+
}
|
|
152
|
+
const validated_input = parse_result.data;
|
|
144
153
|
// step 6: dispatch — transaction for mutations, pool for reads.
|
|
145
154
|
const use_transaction = spec.side_effects;
|
|
146
155
|
const execute = async (effective_db) => {
|
|
@@ -176,7 +176,7 @@ export interface RegisterActionWsResult {
|
|
|
176
176
|
* `ctx.signal` before bubbling out.
|
|
177
177
|
* - Per-message dispatch goes through `perform_action`: pre-authorization
|
|
178
178
|
* auth (401) → authorization phase → post-authorization auth (403) →
|
|
179
|
-
*
|
|
179
|
+
* rate limit (429) → input validation (400) → handler (with transaction
|
|
180
180
|
* wrap iff `spec.side_effects: true`) → DEV output validation.
|
|
181
181
|
* - Authorization phase runs **per message** — role_grant changes during a
|
|
182
182
|
* connection lifetime are picked up on the next message without any
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAE3D,OAAO,EAAU,KAAK,MAAM,IAAI,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAWnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAE3D,OAAO,EAAU,KAAK,MAAM,IAAI,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAWnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAyBtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAIhD,OAAO,EAAE,yBAAyB,EAAE,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIhG,YAAY,EAAE,MAAM,EAAE,CAAC;AAEvB,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAsCvD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,qFAAqF;IACrF,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,aAAa,EAAE,IAAI,CAAC;IACpB,oDAAoD;IACpD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,wFAAwF;IACxF,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,EAAE,EAAE,SAAS,CAAC;IACd,2CAA2C;IAC3C,aAAa,EAAE,IAAI,CAAC;IACpB,kGAAkG;IAClG,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,uBAAuB;IACvC,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;;;;OAOG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;;;;OASG;IACH,EAAE,EAAE,EAAE,CAAC;IACP;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5C;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACjD;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,uBAAuB,KAAG,sBAgbrE,CAAC"}
|
|
@@ -32,8 +32,9 @@ import { get_request_context, require_request_context, token_scope_surface_denia
|
|
|
32
32
|
import { hash_session_token } from '../auth/session_queries.js';
|
|
33
33
|
import { get_client_ip } from '../http/client_ip.js';
|
|
34
34
|
import { flush_pending_effects, flush_post_commit_effects } from '../http/pending_effects.js';
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
35
|
+
import { JsonrpcRequest } from '../http/jsonrpc.js';
|
|
36
|
+
import { dev_only, jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
37
|
+
import { create_jsonrpc_error_response, create_jsonrpc_notification, to_jsonrpc_envelope_id, to_jsonrpc_message_id, to_jsonrpc_params, is_jsonrpc_object, is_jsonrpc_request, is_jsonrpc_request_id } from '../http/jsonrpc_helpers.js';
|
|
37
38
|
import { CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY, AUTH_API_TOKEN_ID_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
38
39
|
import {} from './action_types.js';
|
|
39
40
|
import { compile_action_registry } from './compile_action_registry.js';
|
|
@@ -44,6 +45,39 @@ import { audit_unmatched_peer_response } from './peer_request.js';
|
|
|
44
45
|
import { perform_action, perform_action_result_to_envelope } from './perform_action.js';
|
|
45
46
|
/** Default inactivity window before the server closes a silent socket. */
|
|
46
47
|
export const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60_000;
|
|
48
|
+
/** Send one JSON-RPC error response frame on `ws`. */
|
|
49
|
+
const send_error_response = (ws, id, error) => {
|
|
50
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(id, error)));
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Whether a frame is a reply to a server-initiated request, in the shape the
|
|
54
|
+
* Rust twin's `classify` accepts: `jsonrpc: '2.0'`, **no** string `method`, a
|
|
55
|
+
* string/number `id`, and exactly one of `result` / `error`.
|
|
56
|
+
*
|
|
57
|
+
* The method axis is string-typed like the twin, whose
|
|
58
|
+
* `obj.get("method").and_then(Value::as_str)` reads a present-but-non-string
|
|
59
|
+
* `method` as absent — so `{id, method: 123, result}` is a response on both
|
|
60
|
+
* sides, not a request. The `result` / `error` XOR likewise refuses a frame
|
|
61
|
+
* carrying both, so neither side has to pick a winner between them.
|
|
62
|
+
*
|
|
63
|
+
* Deliberately narrower than the `is_jsonrpc_response` /
|
|
64
|
+
* `is_jsonrpc_error_response` guards, which only ask for a version, an `id`,
|
|
65
|
+
* and a `result` or `error` — those stay loose because a client matching a
|
|
66
|
+
* reply must still recognize an error response carrying `id: null`. Here a
|
|
67
|
+
* loose match would swallow hybrids the twin answers: `{method, id, result}`
|
|
68
|
+
* is a request the dispatcher should run, and `{id, result, error}` or a
|
|
69
|
+
* boolean `id` is `invalid_request`. Everything this rejects falls through to
|
|
70
|
+
* the version guard / notification / envelope steps below.
|
|
71
|
+
*/
|
|
72
|
+
const is_peer_response = (json) => {
|
|
73
|
+
if (!is_jsonrpc_object(json))
|
|
74
|
+
return false;
|
|
75
|
+
if (typeof json.method === 'string')
|
|
76
|
+
return false;
|
|
77
|
+
if (!is_jsonrpc_request_id(json.id))
|
|
78
|
+
return false;
|
|
79
|
+
return 'result' in json !== 'error' in json;
|
|
80
|
+
};
|
|
47
81
|
/**
|
|
48
82
|
* Mount a JSON-RPC WebSocket endpoint that dispatches via the shared
|
|
49
83
|
* `perform_action` core.
|
|
@@ -55,7 +89,7 @@ export const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60_000;
|
|
|
55
89
|
* `ctx.signal` before bubbling out.
|
|
56
90
|
* - Per-message dispatch goes through `perform_action`: pre-authorization
|
|
57
91
|
* auth (401) → authorization phase → post-authorization auth (403) →
|
|
58
|
-
*
|
|
92
|
+
* rate limit (429) → input validation (400) → handler (with transaction
|
|
59
93
|
* wrap iff `spec.side_effects: true`) → DEV output validation.
|
|
60
94
|
* - Authorization phase runs **per message** — role_grant changes during a
|
|
61
95
|
* connection lifetime are picked up on the next message without any
|
|
@@ -219,7 +253,7 @@ export const register_action_ws = (options) => {
|
|
|
219
253
|
catch (error) {
|
|
220
254
|
log.error('on_socket_open failed — closing socket:', error);
|
|
221
255
|
try {
|
|
222
|
-
ws
|
|
256
|
+
send_error_response(ws, null, jsonrpc_error_messages.internal_error());
|
|
223
257
|
}
|
|
224
258
|
catch {
|
|
225
259
|
// ignore — socket may already be dead
|
|
@@ -236,22 +270,23 @@ export const register_action_ws = (options) => {
|
|
|
236
270
|
}
|
|
237
271
|
catch (error) {
|
|
238
272
|
log.error('JSON parse error:', error);
|
|
239
|
-
ws
|
|
273
|
+
send_error_response(ws, null, jsonrpc_error_messages.parse_error());
|
|
240
274
|
return;
|
|
241
275
|
}
|
|
242
276
|
// Batch JSON-RPC is not supported on the WebSocket path.
|
|
243
277
|
if (Array.isArray(json)) {
|
|
244
|
-
ws
|
|
278
|
+
send_error_response(ws, null, jsonrpc_error_messages.invalid_request('batch JSON-RPC requests are not supported on WebSocket'));
|
|
245
279
|
return;
|
|
246
280
|
}
|
|
247
281
|
// Inbound responses to server-initiated requests (`peer/ping`,
|
|
248
282
|
// etc.) — route to the pending-request registry scoped to THIS
|
|
249
|
-
// socket. A response carries
|
|
250
|
-
// `method`, so it precedes the
|
|
251
|
-
// An unmatched id
|
|
252
|
-
//
|
|
253
|
-
// survives a junk
|
|
254
|
-
|
|
283
|
+
// socket. A response carries a string/number `id`, exactly one
|
|
284
|
+
// of `result` / `error`, and no string `method`, so it precedes the
|
|
285
|
+
// request/notification split below. An unmatched id
|
|
286
|
+
// (unsolicited, cross-connection, or already settled) resolves
|
|
287
|
+
// nothing and is dropped, so the read loop survives a junk
|
|
288
|
+
// frame.
|
|
289
|
+
if (is_peer_response(json)) {
|
|
255
290
|
if (captured_connection_id !== undefined) {
|
|
256
291
|
const matched = transport.resolve_peer_response(captured_connection_id, json);
|
|
257
292
|
if (!matched)
|
|
@@ -259,13 +294,32 @@ export const register_action_ws = (options) => {
|
|
|
259
294
|
}
|
|
260
295
|
return;
|
|
261
296
|
}
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
297
|
+
// Envelope version — the first thing the Rust twin's `classify`
|
|
298
|
+
// checks (version → id → method → params). A frame without
|
|
299
|
+
// `jsonrpc: '2.0'` is `invalid_request` on both transports
|
|
300
|
+
// rather than falling through to the notification branch,
|
|
301
|
+
// which would silently drop `{method: 'x'}` and
|
|
302
|
+
// `{jsonrpc: '1.0', method: 'x'}`. It also covers a
|
|
303
|
+
// non-object frame (a scalar has no id to echo — `id: null`).
|
|
304
|
+
// A genuine peer response carries the version and is matched
|
|
305
|
+
// above; a response-shaped frame that misses that shape check
|
|
306
|
+
// falls through here on purpose, so it is answered rather
|
|
307
|
+
// than swallowed.
|
|
308
|
+
if (!is_jsonrpc_object(json)) {
|
|
309
|
+
send_error_response(ws, to_jsonrpc_envelope_id(json), jsonrpc_error_messages.invalid_request());
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Notifications (string method + no id) — `cancel` is
|
|
313
|
+
// intercepted for request-scoped cancellation; other
|
|
314
|
+
// notifications are silenced per JSON-RPC spec (consumer
|
|
315
|
+
// notification handlers are not a feature yet). The method
|
|
316
|
+
// axis is string-typed like the twin's `classify`, so a
|
|
317
|
+
// non-string `method` is answered `invalid_request` below
|
|
318
|
+
// rather than silently dropped as a notification.
|
|
266
319
|
if (!is_jsonrpc_request(json)) {
|
|
267
|
-
|
|
268
|
-
|
|
320
|
+
const notification_method = json.method;
|
|
321
|
+
if (typeof notification_method === 'string' && !('id' in json)) {
|
|
322
|
+
if (notification_method === cancel_action_spec.method) {
|
|
269
323
|
const parsed = CancelNotificationParams.safeParse(json.params);
|
|
270
324
|
if (!parsed.success) {
|
|
271
325
|
log.debug('cancel: invalid params, ignoring', parsed.error.issues);
|
|
@@ -281,17 +335,30 @@ export const register_action_ws = (options) => {
|
|
|
281
335
|
}
|
|
282
336
|
return;
|
|
283
337
|
}
|
|
284
|
-
ws
|
|
338
|
+
send_error_response(ws, to_jsonrpc_message_id(json), jsonrpc_error_messages.invalid_request());
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Envelope validation — the same `JsonrpcRequest.safeParse` the
|
|
342
|
+
// HTTP POST path runs, so a malformed envelope (`params: null`,
|
|
343
|
+
// an array `params`, a boolean `id`) answers `invalid_request`
|
|
344
|
+
// on both transports instead of reaching the dispatcher and
|
|
345
|
+
// surfacing as `invalid_params`. It sits after the
|
|
346
|
+
// notification / cancel block so notifications stay
|
|
347
|
+
// unvalidated (they carry no id to echo) and `cancel` routing
|
|
348
|
+
// precedes it.
|
|
349
|
+
const envelope = JsonrpcRequest.safeParse(json);
|
|
350
|
+
if (!envelope.success) {
|
|
351
|
+
send_error_response(ws, to_jsonrpc_message_id(json), jsonrpc_error_messages.invalid_request(dev_only({ issues: envelope.error.issues })));
|
|
285
352
|
return;
|
|
286
353
|
}
|
|
287
|
-
const { method, id, params } =
|
|
354
|
+
const { method, id, params } = envelope.data;
|
|
288
355
|
// Per-action method lookup — return method_not_found before
|
|
289
356
|
// we engage the dispatch machinery. Specs without a handler
|
|
290
357
|
// (client-only / dispatcher-handled) miss action_map and
|
|
291
358
|
// surface as method_not_found just like unknown methods.
|
|
292
359
|
const action = action_map.get(method);
|
|
293
360
|
if (!action) {
|
|
294
|
-
ws
|
|
361
|
+
send_error_response(ws, id, jsonrpc_error_messages.method_not_found(method));
|
|
295
362
|
return;
|
|
296
363
|
}
|
|
297
364
|
if (artificial_delay > 0) {
|