@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.
Files changed (145) hide show
  1. package/dist/actions/CLAUDE.md +57 -23
  2. package/dist/actions/action_bridge.d.ts.map +1 -1
  3. package/dist/actions/action_codegen.d.ts +1 -1
  4. package/dist/actions/action_codegen.d.ts.map +1 -1
  5. package/dist/actions/action_codegen.js +1 -1
  6. package/dist/actions/{action_peer.d.ts → action_dispatcher.d.ts} +12 -12
  7. package/dist/actions/action_dispatcher.d.ts.map +1 -0
  8. package/dist/actions/{action_peer.js → action_dispatcher.js} +2 -2
  9. package/dist/actions/action_event.d.ts.map +1 -1
  10. package/dist/actions/action_rpc.d.ts +9 -0
  11. package/dist/actions/action_rpc.d.ts.map +1 -1
  12. package/dist/actions/broadcast_api.d.ts +2 -2
  13. package/dist/actions/broadcast_api.d.ts.map +1 -1
  14. package/dist/actions/compile_action_registry.d.ts.map +1 -1
  15. package/dist/actions/frontend_rpc_client.d.ts +3 -3
  16. package/dist/actions/frontend_rpc_client.d.ts.map +1 -1
  17. package/dist/actions/frontend_rpc_client.js +3 -3
  18. package/dist/actions/peer_ping.d.ts +147 -0
  19. package/dist/actions/peer_ping.d.ts.map +1 -0
  20. package/dist/actions/peer_ping.js +189 -0
  21. package/dist/actions/peer_request.d.ts +158 -0
  22. package/dist/actions/peer_request.d.ts.map +1 -0
  23. package/dist/actions/peer_request.js +167 -0
  24. package/dist/actions/perform_action.d.ts +8 -0
  25. package/dist/actions/perform_action.d.ts.map +1 -1
  26. package/dist/actions/perform_action.js +2 -1
  27. package/dist/actions/protocol.d.ts +3 -3
  28. package/dist/actions/protocol.d.ts.map +1 -1
  29. package/dist/actions/protocol.js +9 -4
  30. package/dist/actions/register_action_ws.d.ts.map +1 -1
  31. package/dist/actions/register_action_ws.js +28 -1
  32. package/dist/actions/rpc_client.d.ts +4 -4
  33. package/dist/actions/rpc_client.d.ts.map +1 -1
  34. package/dist/actions/socket.svelte.d.ts.map +1 -1
  35. package/dist/actions/transports.d.ts +1 -1
  36. package/dist/actions/transports_http.d.ts.map +1 -1
  37. package/dist/actions/transports_ws.d.ts +12 -5
  38. package/dist/actions/transports_ws.d.ts.map +1 -1
  39. package/dist/actions/transports_ws.js +36 -6
  40. package/dist/actions/transports_ws_backend.d.ts +34 -1
  41. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  42. package/dist/actions/transports_ws_backend.js +62 -5
  43. package/dist/auth/CLAUDE.md +12 -12
  44. package/dist/auth/actor_lookup_action_specs.d.ts.map +1 -1
  45. package/dist/auth/actor_search_action_specs.d.ts.map +1 -1
  46. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  47. package/dist/auth/admin_actions.d.ts.map +1 -1
  48. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  49. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  50. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  51. package/dist/auth/role_grant_queries.d.ts +31 -12
  52. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  53. package/dist/auth/role_grant_queries.js +36 -12
  54. package/dist/auth/role_schema.d.ts.map +1 -1
  55. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  56. package/dist/db/create_db.d.ts.map +1 -1
  57. package/dist/db/fact_store.d.ts.map +1 -1
  58. package/dist/db/status.d.ts.map +1 -1
  59. package/dist/http/auth_shape.d.ts.map +1 -1
  60. package/dist/http/db_routes.d.ts.map +1 -1
  61. package/dist/http/jsonrpc_helpers.d.ts +1 -1
  62. package/dist/http/jsonrpc_helpers.js +1 -1
  63. package/dist/rate_limiter.d.ts.map +1 -1
  64. package/dist/rate_limiter.js +11 -0
  65. package/dist/server/app_backend.d.ts.map +1 -1
  66. package/dist/server/app_server.d.ts.map +1 -1
  67. package/dist/server/serve_fact_route.d.ts.map +1 -1
  68. package/dist/server/serve_fact_route.js +7 -4
  69. package/dist/testing/CLAUDE.md +18 -15
  70. package/dist/testing/admin_integration.d.ts.map +1 -1
  71. package/dist/testing/adversarial_input.d.ts.map +1 -1
  72. package/dist/testing/adversarial_input.js +3 -1
  73. package/dist/testing/attack_surface.d.ts.map +1 -1
  74. package/dist/testing/audit_completeness.d.ts.map +1 -1
  75. package/dist/testing/cross_backend/action_manifest.d.ts +7 -7
  76. package/dist/testing/cross_backend/action_manifest.js +7 -7
  77. package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
  78. package/dist/testing/cross_backend/capabilities.d.ts +15 -12
  79. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  80. package/dist/testing/cross_backend/capabilities.js +4 -4
  81. package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -1
  82. package/dist/testing/cross_backend/conformance_case.d.ts +2 -0
  83. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  84. package/dist/testing/cross_backend/conformance_case.js +13 -0
  85. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  86. package/dist/testing/cross_backend/conformance_table.js +32 -3
  87. package/dist/testing/cross_backend/cookie_attributes.d.ts +20 -0
  88. package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -0
  89. package/dist/testing/cross_backend/cookie_attributes.js +158 -0
  90. package/dist/testing/cross_backend/credential_header_robustness.d.ts +12 -0
  91. package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -0
  92. package/dist/testing/cross_backend/credential_header_robustness.js +179 -0
  93. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  94. package/dist/testing/cross_backend/default_backend_configs.js +5 -3
  95. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  96. package/dist/testing/cross_backend/full_spine_mount.js +7 -0
  97. package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
  98. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  99. package/dist/testing/cross_backend/method_coverage.d.ts +3 -7
  100. package/dist/testing/cross_backend/method_coverage.d.ts.map +1 -1
  101. package/dist/testing/cross_backend/method_coverage.js +1 -2
  102. package/dist/testing/cross_backend/peer_ping_ws.js +1 -1
  103. package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts +26 -0
  104. package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -0
  105. package/dist/testing/cross_backend/role_grant_offer_enumeration.js +108 -0
  106. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  107. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  108. package/dist/testing/cross_backend/setup.js +2 -4
  109. package/dist/testing/cross_backend/testing_reset_actions.d.ts +0 -13
  110. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  111. package/dist/testing/cross_backend/testing_reset_actions.js +12 -4
  112. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  113. package/dist/testing/cross_backend/ts_spine_backend_config.js +9 -3
  114. package/dist/testing/data_exposure.d.ts.map +1 -1
  115. package/dist/testing/db.d.ts.map +1 -1
  116. package/dist/testing/db_entities.d.ts +15 -0
  117. package/dist/testing/db_entities.d.ts.map +1 -1
  118. package/dist/testing/db_entities.js +16 -1
  119. package/dist/testing/error_coverage.d.ts.map +1 -1
  120. package/dist/testing/error_coverage.js +1 -2
  121. package/dist/testing/integration_helpers.d.ts.map +1 -1
  122. package/dist/testing/rate_limiting.d.ts.map +1 -1
  123. package/dist/testing/round_trip.d.ts.map +1 -1
  124. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  125. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  126. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  127. package/dist/testing/schema_generators.d.ts.map +1 -1
  128. package/dist/testing/schema_generators.js +3 -1
  129. package/dist/testing/schema_parity.d.ts.map +1 -1
  130. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  131. package/dist/testing/surface_invariants.d.ts.map +1 -1
  132. package/dist/testing/transports/bootstrap.d.ts.map +1 -1
  133. package/dist/testing/transports/ws_client.d.ts +1 -1
  134. package/dist/testing/transports/ws_transport.d.ts +1 -1
  135. package/dist/testing/ws_round_trip.d.ts +1 -1
  136. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  137. package/dist/testing/ws_round_trip.js +3 -3
  138. package/dist/ui/CLAUDE.md +6 -3
  139. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  140. package/dist/ui/form_state.svelte.d.ts.map +1 -1
  141. package/dist/ui/role_grant_offers_state.svelte.d.ts +1 -1
  142. package/dist/ui/role_grant_offers_state.svelte.js +1 -1
  143. package/dist/ui/ui_format.d.ts.map +1 -1
  144. package/package.json +2 -2
  145. 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;AAE7E;;;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;;;;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,CAoK7B,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"}
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` and
3
- * `cancel`. Spread these into consumer registrations on both sides of the
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;AAI9C;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAqC,CAAC;AAEzF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC,eAAe,CAEhE,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"}
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Canonical bundles of fuz_app's protocol actions — `heartbeat` and
3
- * `cancel`. Spread these into consumer registrations on both sides of the
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 = [heartbeat_action, cancel_action];
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;AAgBpD,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;AAG9F,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,sBA4VrE,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"}