@fuzdev/fuz_app 0.97.0 → 0.98.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 +57 -23
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +1 -1
- package/dist/actions/{action_peer.d.ts → action_dispatcher.d.ts} +12 -12
- package/dist/actions/action_dispatcher.d.ts.map +1 -0
- package/dist/actions/{action_peer.js → action_dispatcher.js} +2 -2
- package/dist/actions/action_event.d.ts.map +1 -1
- package/dist/actions/action_rpc.d.ts +9 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/broadcast_api.d.ts +2 -2
- package/dist/actions/broadcast_api.d.ts.map +1 -1
- package/dist/actions/compile_action_registry.d.ts.map +1 -1
- package/dist/actions/frontend_rpc_client.d.ts +3 -3
- package/dist/actions/frontend_rpc_client.d.ts.map +1 -1
- package/dist/actions/frontend_rpc_client.js +3 -3
- package/dist/actions/peer_ping.d.ts +147 -0
- package/dist/actions/peer_ping.d.ts.map +1 -0
- package/dist/actions/peer_ping.js +189 -0
- package/dist/actions/peer_request.d.ts +158 -0
- package/dist/actions/peer_request.d.ts.map +1 -0
- package/dist/actions/peer_request.js +167 -0
- package/dist/actions/perform_action.d.ts +8 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +2 -1
- package/dist/actions/protocol.d.ts +3 -3
- package/dist/actions/protocol.d.ts.map +1 -1
- package/dist/actions/protocol.js +9 -4
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +28 -1
- package/dist/actions/rpc_client.d.ts +4 -4
- package/dist/actions/rpc_client.d.ts.map +1 -1
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/transports.d.ts +1 -1
- package/dist/actions/transports_http.d.ts.map +1 -1
- package/dist/actions/transports_ws.d.ts +12 -5
- package/dist/actions/transports_ws.d.ts.map +1 -1
- package/dist/actions/transports_ws.js +36 -6
- package/dist/actions/transports_ws_backend.d.ts +34 -1
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +62 -5
- package/dist/auth/CLAUDE.md +12 -12
- package/dist/auth/actor_lookup_action_specs.d.ts.map +1 -1
- package/dist/auth/actor_search_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.d.ts +31 -12
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +36 -12
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/db/create_db.d.ts.map +1 -1
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/status.d.ts.map +1 -1
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/jsonrpc_helpers.d.ts +1 -1
- package/dist/http/jsonrpc_helpers.js +1 -1
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +11 -0
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +7 -4
- package/dist/testing/CLAUDE.md +18 -15
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +3 -1
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest.d.ts +7 -7
- package/dist/testing/cross_backend/action_manifest.js +7 -7
- package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +15 -12
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +4 -4
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.d.ts +2 -0
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +13 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +32 -3
- package/dist/testing/cross_backend/cookie_attributes.d.ts +20 -0
- package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -0
- package/dist/testing/cross_backend/cookie_attributes.js +158 -0
- package/dist/testing/cross_backend/credential_header_robustness.d.ts +12 -0
- package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -0
- package/dist/testing/cross_backend/credential_header_robustness.js +179 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +5 -3
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +7 -0
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/method_coverage.d.ts +3 -7
- package/dist/testing/cross_backend/method_coverage.d.ts.map +1 -1
- package/dist/testing/cross_backend/method_coverage.js +1 -2
- package/dist/testing/cross_backend/peer_ping_ws.js +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts +26 -0
- package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_offer_enumeration.js +108 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +2 -4
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +0 -13
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +12 -4
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +9 -3
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db_entities.d.ts +15 -0
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +16 -1
- package/dist/testing/error_coverage.d.ts.map +1 -1
- package/dist/testing/error_coverage.js +1 -2
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +3 -1
- package/dist/testing/schema_parity.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/transports/bootstrap.d.ts.map +1 -1
- package/dist/testing/transports/ws_client.d.ts +1 -1
- package/dist/testing/transports/ws_transport.d.ts +1 -1
- package/dist/testing/ws_round_trip.d.ts +1 -1
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +3 -3
- package/dist/ui/CLAUDE.md +6 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/form_state.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.d.ts +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +1 -1
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/actions/action_peer.d.ts.map +0 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `peer/ping` action — the protocol action that exercises the
|
|
3
|
+
* server→client request/response direction of ActionPeer.
|
|
4
|
+
*
|
|
5
|
+
* `peer/ping` is `initiator: 'both'`: the client→server direction overlaps
|
|
6
|
+
* with `heartbeat`, but the new direction this action drives is the reverse —
|
|
7
|
+
* a connected client invokes `peer/ping` over its socket, and the handler
|
|
8
|
+
* turns around and *initiates* a `peer/ping` request back to that same socket
|
|
9
|
+
* (`ctx.request_client`), awaits the client's echo, validates it against
|
|
10
|
+
* `PingResponse`, and returns the validated shape. So one client RPC drives
|
|
11
|
+
* the whole round-trip and every outcome surfaces as that RPC's wire response.
|
|
12
|
+
*
|
|
13
|
+
* **Public auth.** A liveness echo is non-sensitive; the WebSocket upgrade
|
|
14
|
+
* itself still authenticates, so only admitted sockets reach the handler.
|
|
15
|
+
*
|
|
16
|
+
* **No return transport (HTTP RPC).** Over HTTP there is no socket to ping —
|
|
17
|
+
* `ctx.request_client` is absent — so the handler refuses with
|
|
18
|
+
* `peer_no_transport` rather than `method_not_found`. That requires the action
|
|
19
|
+
* to be mounted on the HTTP RPC endpoint as well as the WS endpoint (it is, via
|
|
20
|
+
* the spine's full mount); the manifest excludes it as a protocol action so the
|
|
21
|
+
* cross-impl diff stays apples-to-apples.
|
|
22
|
+
*
|
|
23
|
+
* The `data.reason` discriminators and the `PingResponse` shape are the
|
|
24
|
+
* cross-impl wire contract — twins of the Rust spine's `REASON_PEER_*`
|
|
25
|
+
* constants and `PingResponse` (parity by test, not codegen).
|
|
26
|
+
*
|
|
27
|
+
* **Frontend-safe.** Like `heartbeat.ts` / `cancel.ts`, this module names
|
|
28
|
+
* the dispatcher types (`ActionContext`, `RpcAction`) type-only and builds
|
|
29
|
+
* `peer_ping_action` as a plain literal — it never imports the runtime
|
|
30
|
+
* `action_rpc.ts` (which would drag the HTTP dispatch core + Hono into any
|
|
31
|
+
* frontend that pulls `protocol_action_specs`). The frontend responder
|
|
32
|
+
* (`peer_ping_responder`) lives here too so a real frontend can answer the
|
|
33
|
+
* server's probe.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
import { z } from 'zod';
|
|
38
|
+
import { ThrownJsonrpcError, JSONRPC_ERROR_CODES } from '../http/jsonrpc_errors.js';
|
|
39
|
+
import { DEFAULT_PEER_REQUEST_TIMEOUT } from './peer_request.js';
|
|
40
|
+
/** Wire method name (both directions). Twin of the Rust `PEER_PING_METHOD`. */
|
|
41
|
+
export const PEER_PING_METHOD = 'peer/ping';
|
|
42
|
+
/**
|
|
43
|
+
* Wire-protocol version reported in `peer/ping` replies
|
|
44
|
+
* (`PingResponse.protocol_version`). Twin of the Rust spine's value — bump
|
|
45
|
+
* in lockstep with a breaking peer-protocol change. The far side validates
|
|
46
|
+
* the reply's *shape*, not this value, so it's informational telemetry
|
|
47
|
+
* today, but it's the hook a future protocol negotiation reads.
|
|
48
|
+
*/
|
|
49
|
+
export const PEER_PROTOCOL_VERSION = 1;
|
|
50
|
+
// --- `data.reason` discriminators (cross-impl wire contract) ---
|
|
51
|
+
/** The peer did not reply within the deadline. */
|
|
52
|
+
export const REASON_PEER_TIMEOUT = 'peer_timeout';
|
|
53
|
+
/** The socket closed before the peer replied. */
|
|
54
|
+
export const REASON_PEER_CONNECTION_GONE = 'peer_connection_gone';
|
|
55
|
+
/** No return socket — the action ran over a transport that can't initiate a server→client request (HTTP RPC). */
|
|
56
|
+
export const REASON_PEER_NO_TRANSPORT = 'peer_no_transport';
|
|
57
|
+
/** The per-connection in-flight server→client request cap was hit. */
|
|
58
|
+
export const REASON_PEER_TOO_MANY_IN_FLIGHT = 'peer_too_many_in_flight';
|
|
59
|
+
/** The peer replied, but the payload failed `PingResponse` validation. */
|
|
60
|
+
export const REASON_PEER_PING_INVALID_REPLY = 'peer_ping_invalid_reply';
|
|
61
|
+
/** The peer replied with a `PingResponse` whose nonce didn't echo the issued one. */
|
|
62
|
+
export const REASON_PEER_PING_NONCE_MISMATCH = 'peer_ping_nonce_mismatch';
|
|
63
|
+
/**
|
|
64
|
+
* Input to the client→server `peer/ping` invocation. Both fields optional
|
|
65
|
+
* (mirrors the Rust `PingActionInput`): `nonce` defaults to a server-issued
|
|
66
|
+
* value, `timeout_ms` to `DEFAULT_PEER_REQUEST_TIMEOUT` (clamped shorten-only).
|
|
67
|
+
*/
|
|
68
|
+
export const PingActionInput = z
|
|
69
|
+
.strictObject({
|
|
70
|
+
nonce: z.number().int().optional(),
|
|
71
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
72
|
+
})
|
|
73
|
+
// `.default({})` so omitting `params` entirely (undefined) is accepted — both
|
|
74
|
+
// fields are optional, so an empty invocation is valid (server-issued nonce,
|
|
75
|
+
// default timeout). Mirrors `heartbeat`'s nullary `.default({})`.
|
|
76
|
+
.default({});
|
|
77
|
+
/** Params of the server→client `peer/ping` request frame. Twin of the Rust `PingRequest`. */
|
|
78
|
+
export const PingRequestParams = z.strictObject({
|
|
79
|
+
nonce: z.number().int(),
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* The client's reply shape — also the action's output (the handler returns the
|
|
83
|
+
* validated echo). Twin of the Rust `PingResponse`.
|
|
84
|
+
*/
|
|
85
|
+
export const PingResponse = z.strictObject({
|
|
86
|
+
nonce: z.number().int(),
|
|
87
|
+
protocol_version: z.number().int(),
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* `ActionSpec` for the shared `peer/ping`. `initiator: 'both'` (the
|
|
91
|
+
* client→server invocation drives a server→client request); `auth: public`
|
|
92
|
+
* (liveness is non-sensitive; the upgrade authenticated the socket);
|
|
93
|
+
* `side_effects: false` (no state change — the handler only round-trips a ping).
|
|
94
|
+
*/
|
|
95
|
+
export const peer_ping_action_spec = {
|
|
96
|
+
method: PEER_PING_METHOD,
|
|
97
|
+
kind: 'request_response',
|
|
98
|
+
initiator: 'both',
|
|
99
|
+
auth: { account: 'none', actor: 'none' },
|
|
100
|
+
side_effects: false,
|
|
101
|
+
input: PingActionInput,
|
|
102
|
+
output: PingResponse,
|
|
103
|
+
async: true,
|
|
104
|
+
description: 'Liveness round-trip — the handler pings the originating client back over its socket, ' +
|
|
105
|
+
'validates the echo, and returns it. Refused as no-transport over HTTP RPC.',
|
|
106
|
+
};
|
|
107
|
+
/** Monotonic fallback nonce when the caller omits one — mirrors the Rust `next_nonce`. */
|
|
108
|
+
let next_ping_nonce = 0;
|
|
109
|
+
/** Map a transport-level `PeerRequestError` to the thrown JSON-RPC error the caller sees. */
|
|
110
|
+
const peer_request_error_to_jsonrpc = (error) => {
|
|
111
|
+
switch (error.kind) {
|
|
112
|
+
case 'timeout':
|
|
113
|
+
return new ThrownJsonrpcError(JSONRPC_ERROR_CODES.timeout, 'peer request timed out', {
|
|
114
|
+
reason: REASON_PEER_TIMEOUT,
|
|
115
|
+
});
|
|
116
|
+
case 'connection_gone':
|
|
117
|
+
return new ThrownJsonrpcError(JSONRPC_ERROR_CODES.service_unavailable, 'peer connection gone', { reason: REASON_PEER_CONNECTION_GONE });
|
|
118
|
+
case 'too_many_in_flight':
|
|
119
|
+
return new ThrownJsonrpcError(JSONRPC_ERROR_CODES.queue_overflow, 'too many in-flight peer requests', { reason: REASON_PEER_TOO_MANY_IN_FLIGHT });
|
|
120
|
+
case 'client_error':
|
|
121
|
+
// Forward the client's envelope verbatim so its own code / message /
|
|
122
|
+
// data reach the original caller unchanged.
|
|
123
|
+
return new ThrownJsonrpcError(error.error.code, error.error.message, error.error.data);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Handler — initiates a `peer/ping` request back to the originating client,
|
|
128
|
+
* awaits the reply, validates it against `PingResponse`, and returns it.
|
|
129
|
+
*
|
|
130
|
+
* @throws ThrownJsonrpcError with `data.reason` of `peer_no_transport` (HTTP),
|
|
131
|
+
* `peer_timeout` / `peer_connection_gone` / `peer_too_many_in_flight`
|
|
132
|
+
* (transport failures), the client's own envelope (`client_error`), or
|
|
133
|
+
* `peer_ping_invalid_reply` / `peer_ping_nonce_mismatch` (bad echo).
|
|
134
|
+
*/
|
|
135
|
+
export const peer_ping_handler = async (input, ctx) => {
|
|
136
|
+
if (!ctx.request_client) {
|
|
137
|
+
throw new ThrownJsonrpcError(JSONRPC_ERROR_CODES.invalid_request, 'peer/ping has no return transport', { reason: REASON_PEER_NO_TRANSPORT });
|
|
138
|
+
}
|
|
139
|
+
const nonce = input.nonce ?? ++next_ping_nonce;
|
|
140
|
+
// Shorten-only clamp: a remote caller may make the server give up sooner,
|
|
141
|
+
// never hold the (pooled) connection longer than the default.
|
|
142
|
+
const timeout_ms = input.timeout_ms == null
|
|
143
|
+
? DEFAULT_PEER_REQUEST_TIMEOUT
|
|
144
|
+
: Math.min(input.timeout_ms, DEFAULT_PEER_REQUEST_TIMEOUT);
|
|
145
|
+
const request_params = { nonce };
|
|
146
|
+
const outcome = await ctx.request_client(PEER_PING_METHOD, request_params, { timeout_ms });
|
|
147
|
+
if (!outcome.ok)
|
|
148
|
+
throw peer_request_error_to_jsonrpc(outcome.error);
|
|
149
|
+
const parsed = PingResponse.safeParse(outcome.value);
|
|
150
|
+
if (!parsed.success) {
|
|
151
|
+
throw new ThrownJsonrpcError(JSONRPC_ERROR_CODES.validation_error, 'peer/ping reply failed validation', { reason: REASON_PEER_PING_INVALID_REPLY });
|
|
152
|
+
}
|
|
153
|
+
if (parsed.data.nonce !== nonce) {
|
|
154
|
+
throw new ThrownJsonrpcError(JSONRPC_ERROR_CODES.validation_error, 'peer/ping reply nonce mismatch', { reason: REASON_PEER_PING_NONCE_MISMATCH });
|
|
155
|
+
}
|
|
156
|
+
return parsed.data;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Reusable responder for an inbound *server→client* `peer/ping` request — the
|
|
160
|
+
* mirror of `peer_ping_handler`, run by the **client** (frontend). Validates the
|
|
161
|
+
* request params and echoes a `PingResponse` carrying the issued nonce. Pure +
|
|
162
|
+
* transport-agnostic (the production twin of the cross-process test transport's
|
|
163
|
+
* `echo_responder`); `FrontendWebsocketTransport` wires it so a real frontend
|
|
164
|
+
* answers the server's liveness probe with zero consumer plumbing. Falls back to
|
|
165
|
+
* `nonce: 0` on a malformed request — the server then surfaces a nonce mismatch,
|
|
166
|
+
* the correct outcome for a bad probe.
|
|
167
|
+
*
|
|
168
|
+
* @param params - the inbound request's `params` (`PingRequestParams` shape)
|
|
169
|
+
* @returns the `PingResponse` echo to send back as the reply's `result`
|
|
170
|
+
*/
|
|
171
|
+
export const peer_ping_responder = (params) => {
|
|
172
|
+
const parsed = PingRequestParams.safeParse(params);
|
|
173
|
+
return { nonce: parsed.success ? parsed.data.nonce : 0, protocol_version: PEER_PROTOCOL_VERSION };
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Protocol-action tuple — spread into the server's `actions` array (via
|
|
177
|
+
* `protocol_actions` from `actions/protocol.ts`) so the dispatcher resolves the
|
|
178
|
+
* `peer/ping` handler on both the WS endpoint and (for the no-transport refusal)
|
|
179
|
+
* the HTTP RPC endpoint. A plain `RpcAction` literal (not `rpc_action(...)`) so
|
|
180
|
+
* this module stays free of the runtime `action_rpc.ts` import — the same
|
|
181
|
+
* frontend-safety discipline `heartbeat_action` / `cancel_action` follow; the
|
|
182
|
+
* handler's input/output are already pinned by `peer_ping_handler`'s signature.
|
|
183
|
+
* Usable directly in the spine's `RpcAction[]` full mount and the `Action[]`
|
|
184
|
+
* protocol bundle alike.
|
|
185
|
+
*/
|
|
186
|
+
export const peer_ping_action = {
|
|
187
|
+
spec: peer_ping_action_spec,
|
|
188
|
+
handler: peer_ping_handler,
|
|
189
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server→client peer requests (ActionPeer) — the request/response direction a
|
|
3
|
+
* backend initiates to a connected WebSocket client and awaits a typed reply.
|
|
4
|
+
*
|
|
5
|
+
* `PendingPeerRequests` is the correlation registry: it owns the in-flight map
|
|
6
|
+
* (nested `connection_id → request id` — the nesting **is** the per-connection
|
|
7
|
+
* isolation guarantee), id allocation (`s{n}`-namespaced so a client's own
|
|
8
|
+
* request ids can't collide), the per-request deadline, and the per-connection
|
|
9
|
+
* in-flight cap. It deliberately does **not** touch the socket — the caller
|
|
10
|
+
* (`BackendWebsocketTransport.request_connection`) does the send and threads the
|
|
11
|
+
* reply back via `resolve`. This mirrors the Rust `fuz_realtime::peer` split
|
|
12
|
+
* (the `PendingPeerRequests` registry vs the transport that composes it), and
|
|
13
|
+
* keeps the registry pure data so it unit-tests without a live socket.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
18
|
+
import type { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
19
|
+
import type { JsonrpcErrorObject, JsonrpcErrorResponse, JsonrpcRequestId, JsonrpcRequestParams, JsonrpcResponse, JsonrpcResult } from '../http/jsonrpc.js';
|
|
20
|
+
/**
|
|
21
|
+
* Default deadline (ms) for a server→client peer request before it resolves
|
|
22
|
+
* `timeout`. Twin of the Rust spine's `DEFAULT_PEER_TIMEOUT`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_PEER_REQUEST_TIMEOUT = 10000;
|
|
25
|
+
/**
|
|
26
|
+
* Per-connection cap on concurrent in-flight server→client requests. A caller
|
|
27
|
+
* past it gets `too_many_in_flight` instead of growing the pending map
|
|
28
|
+
* unbounded. Twin of the Rust spine's `DEFAULT_MAX_IN_FLIGHT_PER_CONN`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MAX_IN_FLIGHT_PEER_REQUESTS_PER_CONNECTION = 256;
|
|
31
|
+
/**
|
|
32
|
+
* Why a server→client peer request did not yield a client success reply.
|
|
33
|
+
*
|
|
34
|
+
* - `timeout` — the client did not answer within the deadline.
|
|
35
|
+
* - `connection_gone` — the socket closed (or was never registered) before a reply.
|
|
36
|
+
* - `too_many_in_flight` — the per-connection in-flight cap was hit.
|
|
37
|
+
* - `client_error` — the client answered with a JSON-RPC error; `error` is
|
|
38
|
+
* forwarded verbatim so the initiating handler can surface the client's own
|
|
39
|
+
* code / message / data.
|
|
40
|
+
*/
|
|
41
|
+
export type PeerRequestError = {
|
|
42
|
+
kind: 'timeout';
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'connection_gone';
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'too_many_in_flight';
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'client_error';
|
|
49
|
+
error: JsonrpcErrorObject;
|
|
50
|
+
};
|
|
51
|
+
/** Outcome of a server→client peer request: the client's success `result`, or a `PeerRequestError`. */
|
|
52
|
+
export type PeerRequestOutcome = {
|
|
53
|
+
ok: true;
|
|
54
|
+
value: JsonrpcResult;
|
|
55
|
+
} | {
|
|
56
|
+
ok: false;
|
|
57
|
+
error: PeerRequestError;
|
|
58
|
+
};
|
|
59
|
+
/** Per-call options for a server→client peer request. */
|
|
60
|
+
export interface PeerRequestOptions {
|
|
61
|
+
/**
|
|
62
|
+
* Deadline (ms) before the request resolves `timeout`. Defaults to the
|
|
63
|
+
* registry's `default_timeout_ms` (`DEFAULT_PEER_REQUEST_TIMEOUT`). Untrusted
|
|
64
|
+
* remote-supplied values should be clamped **shorten-only** (never lengthen
|
|
65
|
+
* the server's hold on a pooled connection) — see `actions/peer_ping.ts`.
|
|
66
|
+
*/
|
|
67
|
+
timeout_ms?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Initiate a JSON-RPC request to the connected client and await its reply — the
|
|
71
|
+
* server→client direction of ActionPeer. Threaded onto
|
|
72
|
+
* `ActionContext.request_client` for WebSocket handlers (absent on HTTP RPC,
|
|
73
|
+
* where there is no return socket). Returns a `PeerRequestOutcome`; never throws.
|
|
74
|
+
*/
|
|
75
|
+
export type RequestClient = (method: string, params: JsonrpcRequestParams | undefined, options?: PeerRequestOptions) => Promise<PeerRequestOutcome>;
|
|
76
|
+
/** Options for `PendingPeerRequests`. */
|
|
77
|
+
export interface PendingPeerRequestsOptions {
|
|
78
|
+
/** Per-connection in-flight cap. Defaults to `MAX_IN_FLIGHT_PEER_REQUESTS_PER_CONNECTION`. */
|
|
79
|
+
max_in_flight_per_connection?: number;
|
|
80
|
+
/** Default per-request deadline (ms). Defaults to `DEFAULT_PEER_REQUEST_TIMEOUT`. */
|
|
81
|
+
default_timeout_ms?: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Correlation registry for in-flight server→client requests, nested by
|
|
85
|
+
* connection then by the server-issued request id.
|
|
86
|
+
*
|
|
87
|
+
* The per-connection nesting makes the in-flight count and the close-time drain
|
|
88
|
+
* O(1) and is the isolation boundary — a reply on connection B can never settle
|
|
89
|
+
* a request issued on connection A (it lands in a different inner map). An inner
|
|
90
|
+
* map is removed as soon as it empties, so an idle connection holds no entry.
|
|
91
|
+
* The registry never sends on the socket; the caller does the send between
|
|
92
|
+
* `register` and the first `await`, and routes inbound replies back via
|
|
93
|
+
* `resolve`.
|
|
94
|
+
*/
|
|
95
|
+
export declare class PendingPeerRequests {
|
|
96
|
+
#private;
|
|
97
|
+
constructor(options?: PendingPeerRequestsOptions);
|
|
98
|
+
/**
|
|
99
|
+
* Register a pending request for `connection_id` with a deadline, returning
|
|
100
|
+
* its server-issued `s{n}` id and the outcome promise (settled by a reply,
|
|
101
|
+
* the deadline, or `drain`). Returns `null` when the connection is at its
|
|
102
|
+
* in-flight cap — the caller maps that to `too_many_in_flight`. The promise
|
|
103
|
+
* executor runs synchronously, so the entry is registered before this returns.
|
|
104
|
+
*
|
|
105
|
+
* @returns the allocated id + outcome promise, or `null` at the cap
|
|
106
|
+
* @mutates this - inserts an entry into `#pending`
|
|
107
|
+
*/
|
|
108
|
+
register(connection_id: Uuid, timeout_ms?: number): {
|
|
109
|
+
id: JsonrpcRequestId;
|
|
110
|
+
outcome: Promise<PeerRequestOutcome>;
|
|
111
|
+
} | null;
|
|
112
|
+
/**
|
|
113
|
+
* Settle the pending request matching an inbound reply. A success response
|
|
114
|
+
* resolves `{ok: true, value: result}`; an error response resolves
|
|
115
|
+
* `{ok: false, error: {kind: 'client_error', error}}` (forwarded verbatim).
|
|
116
|
+
*
|
|
117
|
+
* Returns `false` when no entry matches — an unsolicited, cross-connection,
|
|
118
|
+
* or already-settled reply — so the caller drops it.
|
|
119
|
+
*
|
|
120
|
+
* @returns whether a pending request was settled
|
|
121
|
+
* @mutates this - clears the matched entry from `#pending`
|
|
122
|
+
*/
|
|
123
|
+
resolve(connection_id: Uuid, response: JsonrpcResponse | JsonrpcErrorResponse): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Force-settle one pending request with an explicit outcome (the send-failure
|
|
126
|
+
* path uses this for `connection_gone`). Clears the timer, drops the entry,
|
|
127
|
+
* resolves the promise. Idempotent — a no-op if the entry is already gone.
|
|
128
|
+
*
|
|
129
|
+
* @mutates this - clears the entry from `#pending`
|
|
130
|
+
*/
|
|
131
|
+
settle(connection_id: Uuid, id: JsonrpcRequestId, outcome: PeerRequestOutcome): void;
|
|
132
|
+
/**
|
|
133
|
+
* Settle every pending request on a closing connection as `connection_gone`.
|
|
134
|
+
* O(1) — drops the connection's inner map in one hop.
|
|
135
|
+
*
|
|
136
|
+
* @mutates this - removes the connection's entry from `#pending`
|
|
137
|
+
*/
|
|
138
|
+
drain(connection_id: Uuid): void;
|
|
139
|
+
/**
|
|
140
|
+
* In-flight request count — for `connection_id` when given, else across all
|
|
141
|
+
* connections. Telemetry / tests.
|
|
142
|
+
*/
|
|
143
|
+
size(connection_id?: Uuid): number;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Sampled, bounded audit for an inbound response that matched no pending request
|
|
147
|
+
* on its connection — an unsolicited `{id, result}`, a cross-connection id echo,
|
|
148
|
+
* or a late/duplicate reply for an already-settled id. Auditing **every**
|
|
149
|
+
* rejected frame would let a junk flood turn the log into the DoS target, so
|
|
150
|
+
* this warns on the first few then samples 1-in-256. Twin of the Rust
|
|
151
|
+
* `fuz_realtime::peer::audit_unmatched_response`.
|
|
152
|
+
*
|
|
153
|
+
* @param log - the WS dispatcher's logger
|
|
154
|
+
* @param connection_id - the socket the unmatched reply arrived on
|
|
155
|
+
* @param id - the reply's echoed id (`null` when absent)
|
|
156
|
+
*/
|
|
157
|
+
export declare const audit_unmatched_peer_response: (log: Logger, connection_id: Uuid, id: JsonrpcRequestId | null) => void;
|
|
158
|
+
//# sourceMappingURL=peer_request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer_request.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/peer_request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,KAAK,EACX,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,MAAM,oBAAoB,CAAC;AAG5B;;;GAGG;AACH,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,0CAA0C,MAAM,CAAC;AAE9D;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GACzB;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GACjB;IAAC,IAAI,EAAE,iBAAiB,CAAA;CAAC,GACzB;IAAC,IAAI,EAAE,oBAAoB,CAAA;CAAC,GAC5B;IAAC,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAC,CAAC;AAErD,uGAAuG;AACvG,MAAM,MAAM,kBAAkB,GAC3B;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAC,GAChC;IAAC,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAC,CAAC;AAExC,yDAAyD;AACzD,MAAM,WAAW,kBAAkB;IAClC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,CAC3B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,oBAAoB,GAAG,SAAS,EACxC,OAAO,CAAC,EAAE,kBAAkB,KACxB,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAUjC,yCAAyC;AACzC,MAAM,WAAW,0BAA0B;IAC1C,8FAA8F;IAC9F,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,qFAAqF;IACrF,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAmB;;gBAUnB,OAAO,CAAC,EAAE,0BAA0B;IAMhD;;;;;;;;;OASG;IACH,QAAQ,CACP,aAAa,EAAE,IAAI,EACnB,UAAU,CAAC,EAAE,MAAM,GACjB;QAAC,EAAE,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAA;KAAC,GAAG,IAAI;IAkBtE;;;;;;;;;;OAUG;IACH,OAAO,CAAC,aAAa,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,GAAG,oBAAoB,GAAG,OAAO;IAWvF;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,EAAE,IAAI,EAAE,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAUpF;;;;;OAKG;IACH,KAAK,CAAC,aAAa,EAAE,IAAI,GAAG,IAAI;IAUhC;;;OAGG;IACH,IAAI,CAAC,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM;CAMlC;AAKD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,GACzC,KAAK,MAAM,EACX,eAAe,IAAI,EACnB,IAAI,gBAAgB,GAAG,IAAI,KACzB,IAQF,CAAC"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server→client peer requests (ActionPeer) — the request/response direction a
|
|
3
|
+
* backend initiates to a connected WebSocket client and awaits a typed reply.
|
|
4
|
+
*
|
|
5
|
+
* `PendingPeerRequests` is the correlation registry: it owns the in-flight map
|
|
6
|
+
* (nested `connection_id → request id` — the nesting **is** the per-connection
|
|
7
|
+
* isolation guarantee), id allocation (`s{n}`-namespaced so a client's own
|
|
8
|
+
* request ids can't collide), the per-request deadline, and the per-connection
|
|
9
|
+
* in-flight cap. It deliberately does **not** touch the socket — the caller
|
|
10
|
+
* (`BackendWebsocketTransport.request_connection`) does the send and threads the
|
|
11
|
+
* reply back via `resolve`. This mirrors the Rust `fuz_realtime::peer` split
|
|
12
|
+
* (the `PendingPeerRequests` registry vs the transport that composes it), and
|
|
13
|
+
* keeps the registry pure data so it unit-tests without a live socket.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { is_jsonrpc_error_response } from '../http/jsonrpc_helpers.js';
|
|
18
|
+
/**
|
|
19
|
+
* Default deadline (ms) for a server→client peer request before it resolves
|
|
20
|
+
* `timeout`. Twin of the Rust spine's `DEFAULT_PEER_TIMEOUT`.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_PEER_REQUEST_TIMEOUT = 10_000;
|
|
23
|
+
/**
|
|
24
|
+
* Per-connection cap on concurrent in-flight server→client requests. A caller
|
|
25
|
+
* past it gets `too_many_in_flight` instead of growing the pending map
|
|
26
|
+
* unbounded. Twin of the Rust spine's `DEFAULT_MAX_IN_FLIGHT_PER_CONN`.
|
|
27
|
+
*/
|
|
28
|
+
export const MAX_IN_FLIGHT_PEER_REQUESTS_PER_CONNECTION = 256;
|
|
29
|
+
/**
|
|
30
|
+
* Correlation registry for in-flight server→client requests, nested by
|
|
31
|
+
* connection then by the server-issued request id.
|
|
32
|
+
*
|
|
33
|
+
* The per-connection nesting makes the in-flight count and the close-time drain
|
|
34
|
+
* O(1) and is the isolation boundary — a reply on connection B can never settle
|
|
35
|
+
* a request issued on connection A (it lands in a different inner map). An inner
|
|
36
|
+
* map is removed as soon as it empties, so an idle connection holds no entry.
|
|
37
|
+
* The registry never sends on the socket; the caller does the send between
|
|
38
|
+
* `register` and the first `await`, and routes inbound replies back via
|
|
39
|
+
* `resolve`.
|
|
40
|
+
*/
|
|
41
|
+
export class PendingPeerRequests {
|
|
42
|
+
#pending = new Map();
|
|
43
|
+
// Monotonic counter for server-issued ids. Serialized as `s{n}` so a client's
|
|
44
|
+
// own (self-chosen) request ids on a bidirectional socket can never collide.
|
|
45
|
+
#next_id = 0;
|
|
46
|
+
#max_in_flight;
|
|
47
|
+
#default_timeout_ms;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.#max_in_flight =
|
|
50
|
+
options?.max_in_flight_per_connection ?? MAX_IN_FLIGHT_PEER_REQUESTS_PER_CONNECTION;
|
|
51
|
+
this.#default_timeout_ms = options?.default_timeout_ms ?? DEFAULT_PEER_REQUEST_TIMEOUT;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Register a pending request for `connection_id` with a deadline, returning
|
|
55
|
+
* its server-issued `s{n}` id and the outcome promise (settled by a reply,
|
|
56
|
+
* the deadline, or `drain`). Returns `null` when the connection is at its
|
|
57
|
+
* in-flight cap — the caller maps that to `too_many_in_flight`. The promise
|
|
58
|
+
* executor runs synchronously, so the entry is registered before this returns.
|
|
59
|
+
*
|
|
60
|
+
* @returns the allocated id + outcome promise, or `null` at the cap
|
|
61
|
+
* @mutates this - inserts an entry into `#pending`
|
|
62
|
+
*/
|
|
63
|
+
register(connection_id, timeout_ms) {
|
|
64
|
+
const existing = this.#pending.get(connection_id);
|
|
65
|
+
if ((existing?.size ?? 0) >= this.#max_in_flight)
|
|
66
|
+
return null;
|
|
67
|
+
const conn_pending = existing ?? new Map();
|
|
68
|
+
if (!existing)
|
|
69
|
+
this.#pending.set(connection_id, conn_pending);
|
|
70
|
+
const id = `s${++this.#next_id}`;
|
|
71
|
+
const ms = timeout_ms ?? this.#default_timeout_ms;
|
|
72
|
+
const outcome = new Promise((resolve) => {
|
|
73
|
+
const timer = setTimeout(() => this.settle(connection_id, id, { ok: false, error: { kind: 'timeout' } }), ms);
|
|
74
|
+
conn_pending.set(id, { resolve, timer });
|
|
75
|
+
});
|
|
76
|
+
return { id, outcome };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Settle the pending request matching an inbound reply. A success response
|
|
80
|
+
* resolves `{ok: true, value: result}`; an error response resolves
|
|
81
|
+
* `{ok: false, error: {kind: 'client_error', error}}` (forwarded verbatim).
|
|
82
|
+
*
|
|
83
|
+
* Returns `false` when no entry matches — an unsolicited, cross-connection,
|
|
84
|
+
* or already-settled reply — so the caller drops it.
|
|
85
|
+
*
|
|
86
|
+
* @returns whether a pending request was settled
|
|
87
|
+
* @mutates this - clears the matched entry from `#pending`
|
|
88
|
+
*/
|
|
89
|
+
resolve(connection_id, response) {
|
|
90
|
+
const { id } = response;
|
|
91
|
+
if (id == null)
|
|
92
|
+
return false;
|
|
93
|
+
if (!this.#pending.get(connection_id)?.has(id))
|
|
94
|
+
return false;
|
|
95
|
+
const outcome = is_jsonrpc_error_response(response)
|
|
96
|
+
? { ok: false, error: { kind: 'client_error', error: response.error } }
|
|
97
|
+
: { ok: true, value: response.result };
|
|
98
|
+
this.settle(connection_id, id, outcome);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Force-settle one pending request with an explicit outcome (the send-failure
|
|
103
|
+
* path uses this for `connection_gone`). Clears the timer, drops the entry,
|
|
104
|
+
* resolves the promise. Idempotent — a no-op if the entry is already gone.
|
|
105
|
+
*
|
|
106
|
+
* @mutates this - clears the entry from `#pending`
|
|
107
|
+
*/
|
|
108
|
+
settle(connection_id, id, outcome) {
|
|
109
|
+
const conn_pending = this.#pending.get(connection_id);
|
|
110
|
+
const entry = conn_pending?.get(id);
|
|
111
|
+
if (!entry || !conn_pending)
|
|
112
|
+
return;
|
|
113
|
+
clearTimeout(entry.timer);
|
|
114
|
+
conn_pending.delete(id);
|
|
115
|
+
if (conn_pending.size === 0)
|
|
116
|
+
this.#pending.delete(connection_id);
|
|
117
|
+
entry.resolve(outcome);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Settle every pending request on a closing connection as `connection_gone`.
|
|
121
|
+
* O(1) — drops the connection's inner map in one hop.
|
|
122
|
+
*
|
|
123
|
+
* @mutates this - removes the connection's entry from `#pending`
|
|
124
|
+
*/
|
|
125
|
+
drain(connection_id) {
|
|
126
|
+
const conn_pending = this.#pending.get(connection_id);
|
|
127
|
+
if (!conn_pending)
|
|
128
|
+
return;
|
|
129
|
+
for (const entry of conn_pending.values()) {
|
|
130
|
+
clearTimeout(entry.timer);
|
|
131
|
+
entry.resolve({ ok: false, error: { kind: 'connection_gone' } });
|
|
132
|
+
}
|
|
133
|
+
this.#pending.delete(connection_id);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* In-flight request count — for `connection_id` when given, else across all
|
|
137
|
+
* connections. Telemetry / tests.
|
|
138
|
+
*/
|
|
139
|
+
size(connection_id) {
|
|
140
|
+
if (connection_id !== undefined)
|
|
141
|
+
return this.#pending.get(connection_id)?.size ?? 0;
|
|
142
|
+
let total = 0;
|
|
143
|
+
for (const conn_pending of this.#pending.values())
|
|
144
|
+
total += conn_pending.size;
|
|
145
|
+
return total;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Module-scope sampling counter for unmatched-response auditing.
|
|
149
|
+
let unmatched_peer_responses = 0;
|
|
150
|
+
/**
|
|
151
|
+
* Sampled, bounded audit for an inbound response that matched no pending request
|
|
152
|
+
* on its connection — an unsolicited `{id, result}`, a cross-connection id echo,
|
|
153
|
+
* or a late/duplicate reply for an already-settled id. Auditing **every**
|
|
154
|
+
* rejected frame would let a junk flood turn the log into the DoS target, so
|
|
155
|
+
* this warns on the first few then samples 1-in-256. Twin of the Rust
|
|
156
|
+
* `fuz_realtime::peer::audit_unmatched_response`.
|
|
157
|
+
*
|
|
158
|
+
* @param log - the WS dispatcher's logger
|
|
159
|
+
* @param connection_id - the socket the unmatched reply arrived on
|
|
160
|
+
* @param id - the reply's echoed id (`null` when absent)
|
|
161
|
+
*/
|
|
162
|
+
export const audit_unmatched_peer_response = (log, connection_id, id) => {
|
|
163
|
+
const n = unmatched_peer_responses++;
|
|
164
|
+
if (n < 8 || n % 256 === 0) {
|
|
165
|
+
log.warn('ws: dropped unmatched peer response (unsolicited, cross-connection, or already settled)', { connection_id, id, total: n + 1 });
|
|
166
|
+
}
|
|
167
|
+
};
|
|
@@ -45,6 +45,7 @@ import type { Db } from '../db/db.js';
|
|
|
45
45
|
import { type JsonrpcRequestId, type JsonrpcErrorObject } from '../http/jsonrpc.js';
|
|
46
46
|
import type { RateLimiter } from '../rate_limiter.js';
|
|
47
47
|
import type { RpcAction } from './action_rpc.js';
|
|
48
|
+
import type { RequestClient } from './peer_request.js';
|
|
48
49
|
/**
|
|
49
50
|
* Per-call inputs to `perform_action`. Each transport assembles this from
|
|
50
51
|
* its wire envelope + connection identity.
|
|
@@ -68,6 +69,13 @@ export interface PerformActionInput {
|
|
|
68
69
|
notify: (method: string, params: unknown) => void;
|
|
69
70
|
/** Stable per-socket id on WS; `undefined` on HTTP. */
|
|
70
71
|
connection_id?: Uuid;
|
|
72
|
+
/**
|
|
73
|
+
* Initiate a server→client request on the originating WS socket and await
|
|
74
|
+
* the typed reply (ActionPeer). Present only on WebSocket dispatch;
|
|
75
|
+
* `undefined` on HTTP RPC (no return socket). Handlers must handle its
|
|
76
|
+
* absence — e.g. `peer/ping` surfaces `peer_no_transport`.
|
|
77
|
+
*/
|
|
78
|
+
request_client?: RequestClient;
|
|
71
79
|
/**
|
|
72
80
|
* Test-harness escape hatch. When set, the live authorization phase is
|
|
73
81
|
* skipped and `request_context` is used directly for post-authorization
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAC,KAAK,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAGpC,OAAO,EAEN,KAAK,gBAAgB,EAErB,KAAK,kBAAkB,EACvB,MAAM,oBAAoB,CAAC;AAa5B,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAA+B,SAAS,EAAC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAC,KAAK,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAGpC,OAAO,EAEN,KAAK,gBAAgB,EAErB,KAAK,kBAAkB,EACvB,MAAM,oBAAoB,CAAC;AAa5B,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAA+B,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC7E,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAErD;;;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,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;QAAC,eAAe,EAAE,cAAc,GAAG,IAAI,CAAA;KAAC,CAAC;CAClD;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,GAC5B;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,GAC7B;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAC;AAE9D;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAC1B,OAAO,kBAAkB,EACzB,MAAM,iBAAiB,KACrB,OAAO,CAAC,mBAAmB,CAsK7B,CAAC;AAoFF;;;GAGG;AACH,eAAO,MAAM,iCAAiC,GAC7C,IAAI,gBAAgB,EACpB,QAAQ,mBAAmB,KACzB;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,gBAAgB,CAAA;CAAC,GAAG,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,GAAG;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAC,CAK5F,CAAC"}
|
|
@@ -57,7 +57,7 @@ import { is_public_auth } from '../http/auth_shape.js';
|
|
|
57
57
|
* still fires when the harness omits `account_id`.
|
|
58
58
|
*/
|
|
59
59
|
export const perform_action = async (input, deps) => {
|
|
60
|
-
const { action, raw_params, request_id: id, account_id, credential_type, client_ip, signal, notify, connection_id, preset, } = input;
|
|
60
|
+
const { action, raw_params, request_id: id, account_id, credential_type, client_ip, signal, notify, connection_id, request_client, preset, } = input;
|
|
61
61
|
const { db, pending_effects, post_commit_effects, log, action_ip_rate_limiter, action_account_rate_limiter, } = deps;
|
|
62
62
|
const { spec, handler } = action;
|
|
63
63
|
const action_auth = spec.auth;
|
|
@@ -139,6 +139,7 @@ export const perform_action = async (input, deps) => {
|
|
|
139
139
|
auth: request_context,
|
|
140
140
|
request_id: id,
|
|
141
141
|
connection_id,
|
|
142
|
+
request_client,
|
|
142
143
|
db: effective_db,
|
|
143
144
|
pending_effects,
|
|
144
145
|
post_commit_effects,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical bundles of fuz_app's protocol actions — `heartbeat`
|
|
3
|
-
* `
|
|
4
|
-
* wire so the registries stay symmetric without per-consumer plumbing.
|
|
2
|
+
* Canonical bundles of fuz_app's protocol actions — `heartbeat`, `cancel`,
|
|
3
|
+
* and `peer/ping`. Spread these into consumer registrations on both sides of
|
|
4
|
+
* the wire so the registries stay symmetric without per-consumer plumbing.
|
|
5
5
|
*
|
|
6
6
|
* Protocol actions are wire-protocol concerns (liveness, abort) shipped by
|
|
7
7
|
* fuz_app, not consumer domain logic. The split is intentional: the server
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,mBAAmB,CAAC;AAK9C;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAIlD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC,eAAe,CAEhE,CAAC"}
|
package/dist/actions/protocol.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical bundles of fuz_app's protocol actions — `heartbeat`
|
|
3
|
-
* `
|
|
4
|
-
* wire so the registries stay symmetric without per-consumer plumbing.
|
|
2
|
+
* Canonical bundles of fuz_app's protocol actions — `heartbeat`, `cancel`,
|
|
3
|
+
* and `peer/ping`. Spread these into consumer registrations on both sides of
|
|
4
|
+
* the wire so the registries stay symmetric without per-consumer plumbing.
|
|
5
5
|
*
|
|
6
6
|
* Protocol actions are wire-protocol concerns (liveness, abort) shipped by
|
|
7
7
|
* fuz_app, not consumer domain logic. The split is intentional: the server
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { cancel_action } from './cancel.js';
|
|
22
22
|
import { heartbeat_action } from './heartbeat.js';
|
|
23
|
+
import { peer_ping_action } from './peer_ping.js';
|
|
23
24
|
/**
|
|
24
25
|
* Canonical protocol `{spec, handler}` tuples for the server's
|
|
25
26
|
* `register_action_ws` `actions` array. Spread before consumer-owned actions
|
|
@@ -29,7 +30,11 @@ import { heartbeat_action } from './heartbeat.js';
|
|
|
29
30
|
* register_action_ws({actions: [...protocol_actions, ...consumer_actions], ...})
|
|
30
31
|
* ```
|
|
31
32
|
*/
|
|
32
|
-
export const protocol_actions = [
|
|
33
|
+
export const protocol_actions = [
|
|
34
|
+
heartbeat_action,
|
|
35
|
+
cancel_action,
|
|
36
|
+
peer_ping_action,
|
|
37
|
+
];
|
|
33
38
|
/**
|
|
34
39
|
* Canonical protocol specs for `ActionRegistry` construction on the
|
|
35
40
|
* frontend. Spread before consumer-owned specs so dispatcher-owned methods
|
|
@@ -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,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAC/B,OAAO,KAAK,EAAC,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAC1E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAUjD,OAAO,KAAK,EAAC,WAAW,EAAC,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,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAC/B,OAAO,KAAK,EAAC,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAC1E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAUjD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAkBpD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,EAAC,KAAK,MAAM,EAAC,MAAM,mBAAmB,CAAC;AAI9C,OAAO,EAAC,yBAAyB,EAAE,KAAK,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AAI9F,YAAY,EAAC,MAAM,EAAC,CAAC;AAErB,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;;;;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,sBA6XrE,CAAC"}
|