@fuzdev/fuz_app 0.118.0 → 0.119.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 +301 -73
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +16 -1
- package/dist/actions/connection_closer.d.ts +136 -51
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +133 -34
- package/dist/actions/register_action_ws.d.ts +176 -24
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +581 -241
- package/dist/actions/register_ws_endpoint.d.ts +1 -0
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +1 -0
- package/dist/actions/socket.svelte.d.ts +43 -9
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +59 -8
- package/dist/actions/transports.d.ts +55 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +55 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +87 -111
- package/dist/actions/transports_ws_backend.d.ts +212 -30
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +327 -70
- package/dist/actions/ws_endpoint_spec.d.ts +26 -13
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +113 -32
- package/dist/auth/account_actions.d.ts +12 -18
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +47 -33
- package/dist/auth/account_routes.d.ts +5 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +40 -36
- package/dist/auth/account_schema.d.ts +27 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +22 -0
- package/dist/auth/admin_actions.d.ts +8 -17
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +33 -21
- package/dist/auth/api_token_queries.d.ts +23 -2
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +39 -3
- package/dist/auth/audit_emitter.d.ts +61 -38
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +57 -14
- package/dist/auth/audit_log_route_schema.js +7 -7
- package/dist/auth/audit_log_routes.d.ts +31 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +85 -11
- package/dist/auth/audit_log_schema.d.ts +48 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +58 -0
- package/dist/auth/cleanup.d.ts +203 -28
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +222 -26
- package/dist/auth/deps.d.ts +28 -0
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/resolved_auth.d.ts +105 -0
- package/dist/auth/resolved_auth.d.ts.map +1 -0
- package/dist/auth/resolved_auth.js +124 -0
- package/dist/auth/session_middleware.d.ts +8 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +9 -3
- package/dist/auth/session_queries.d.ts +31 -6
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +31 -6
- package/dist/auth/standard_rpc_actions.d.ts +9 -9
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +3 -3
- package/dist/db/CLAUDE.md +7 -1
- package/dist/db/status.d.ts +58 -1
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -77
- package/dist/http/CLAUDE.md +7 -3
- package/dist/http/db_routes.d.ts +6 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +2 -2
- package/dist/periodic.d.ts +80 -0
- package/dist/periodic.d.ts.map +1 -0
- package/dist/periodic.js +122 -0
- package/dist/realtime/sse_auth_guard.d.ts +79 -49
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +103 -73
- package/dist/realtime/subscriber_registry.d.ts +119 -8
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +184 -25
- package/dist/server/app_backend.d.ts +12 -1
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +5 -2
- package/dist/server/app_server.d.ts +55 -15
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +41 -13
- package/dist/testing/CLAUDE.md +140 -48
- package/dist/testing/app_server.d.ts +2 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -2
- package/dist/testing/audit_drift_guard.d.ts +1 -48
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +0 -42
- package/dist/testing/connection_closer_helpers.d.ts +9 -22
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +11 -17
- package/dist/testing/cross_backend/capabilities.d.ts +67 -13
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +27 -12
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +21 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
- package/dist/testing/cross_backend/session_cap.d.ts +13 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +55 -19
- package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +93 -26
- package/dist/testing/cross_backend/testing_server_bun.js +4 -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 +10 -2
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +156 -19
- package/dist/testing/sse_round_trip.d.ts +6 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/sse_transport.d.ts +1 -1
- package/dist/testing/transports/ws_client.d.ts +7 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_raw_client.d.ts +51 -0
- package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_raw_client.js +156 -0
- package/dist/testing/transports/ws_transport.d.ts +28 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +45 -1
- package/dist/testing/ws_round_trip.d.ts +8 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -4
- package/dist/ui/AdminAccounts.svelte +4 -3
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +3 -1
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +22 -6
- package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -0
- package/dist/ui/auth_state.svelte.d.ts +25 -1
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +28 -1
- package/package.json +1 -1
|
@@ -1,33 +1,166 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Backend WebSocket transport — manages server-side WebSocket connections
|
|
3
|
-
* with session tracking
|
|
3
|
+
* with session tracking, revocation support, a per-account connection cap,
|
|
4
|
+
* and two-phase registration.
|
|
5
|
+
*
|
|
6
|
+
* ## Two-phase registration
|
|
7
|
+
*
|
|
8
|
+
* An upgrade resolves its credential before the socket exists, and every
|
|
9
|
+
* revocation closes the connections *registered* under the revoked
|
|
10
|
+
* credential. A one-step registration therefore has a window: a revocation
|
|
11
|
+
* landing between the credential check and the insert closes nothing, and the
|
|
12
|
+
* upgrade is admitted on a dead credential — for the socket's lifetime, since
|
|
13
|
+
* per-message dispatch never re-reads the session or token.
|
|
14
|
+
*
|
|
15
|
+
* Registration is split to close it:
|
|
16
|
+
*
|
|
17
|
+
* 1. `BackendWebsocketTransport.register_pending` inserts the entry
|
|
18
|
+
* **un-admitted**. It is closeable from that instant — `close_sockets_for_*`
|
|
19
|
+
* match pending entries deliberately — and is otherwise inert: it evicts
|
|
20
|
+
* nothing, the cap does not count it, no broadcast / `send_to_account` /
|
|
21
|
+
* `request_connection` reaches it, and the count accessors and
|
|
22
|
+
* `is_registered` do not see it (`get_pending_connection_count` does).
|
|
23
|
+
* 2. The caller re-checks the credential against the database
|
|
24
|
+
* (`revalidate_resolved_auth` in `auth/resolved_auth.ts`).
|
|
25
|
+
* 3. `BackendWebsocketTransport.admit` flips the entry to admitted — or
|
|
26
|
+
* refuses when a revocation closed it in the meantime, evicting nothing.
|
|
27
|
+
* The cap's eviction happens here, so a connection that is never admitted
|
|
28
|
+
* never closes a live one, and any number of pending registrations on one
|
|
29
|
+
* account leave its admitted connections alone.
|
|
30
|
+
*
|
|
31
|
+
* A revocation whose close ran **after** step 1 finds the entry, pending or
|
|
32
|
+
* admitted, and closes it; one that **committed before** step 2 is seen by the
|
|
33
|
+
* re-read, which refuses. Nothing is delivered to a pending entry rather than
|
|
34
|
+
* queued for it, so a refusal never has to promise that a queue a revoked
|
|
35
|
+
* credential would have read is dropped unread, and a broadcast's returned
|
|
36
|
+
* count names only connections that can receive it. The cost is a slightly
|
|
37
|
+
* wider blind spot at connect: a server-initiated message sent between the
|
|
38
|
+
* handshake and admission — one credential re-check later — is not delivered,
|
|
39
|
+
* just as one sent before the handshake is not.
|
|
40
|
+
*
|
|
41
|
+
* `add_connection` stays as the one-step form (register and admit together)
|
|
42
|
+
* for a connection with no revocable credential behind it — tests and
|
|
43
|
+
* harnesses that register a socket directly. An upgrade that authenticated a session or token
|
|
44
|
+
* uses the two-phase form. The twin of the Rust `fuz_realtime`
|
|
45
|
+
* `ConnectionRegistry`.
|
|
4
46
|
*
|
|
5
47
|
* @module
|
|
6
48
|
*/
|
|
7
49
|
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
50
|
+
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
8
51
|
import { create_uuid } from '@fuzdev/fuz_util/id.ts';
|
|
9
52
|
import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
10
53
|
import { create_jsonrpc_error_response, create_jsonrpc_request, to_jsonrpc_message_id, is_jsonrpc_request } from '../http/jsonrpc_helpers.js';
|
|
11
|
-
import { WS_CLOSE_SESSION_REVOKED } from './transports.js';
|
|
54
|
+
import { WS_CLOSE_CONNECTION_LIMIT, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON } from './transports.js';
|
|
12
55
|
import { PendingPeerRequests } from './peer_request.js';
|
|
13
56
|
/** Type guard for `FilterableBroadcastTransport`. */
|
|
14
57
|
export const is_filterable_broadcast_transport = (transport) => 'broadcast_filtered' in transport &&
|
|
15
58
|
typeof transport.broadcast_filtered === 'function';
|
|
59
|
+
/**
|
|
60
|
+
* Default per-account WebSocket connection cap, applied by
|
|
61
|
+
* `BackendWebsocketTransport` unless overridden via
|
|
62
|
+
* `BackendWebsocketTransportOptions.max_connections_per_account`.
|
|
63
|
+
*
|
|
64
|
+
* Matches the account-wide ceiling the audit SSE cap implies (the default
|
|
65
|
+
* session cap, `DEFAULT_MAX_SESSIONS`, times `AUDIT_LOG_SSE_MAX_PER_SCOPE`
|
|
66
|
+
* streams per session) — generous enough that a user's tabs and tools never
|
|
67
|
+
* meet it; only an abusive or leaking client does. The twin of the Rust
|
|
68
|
+
* spine's `DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT`.
|
|
69
|
+
*/
|
|
70
|
+
export const DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT = 50;
|
|
16
71
|
export class BackendWebsocketTransport {
|
|
17
72
|
transport_name = 'backend_websocket_rpc';
|
|
18
|
-
//
|
|
73
|
+
// Per-account connection cap; `null` = uncapped.
|
|
74
|
+
#max_connections_per_account;
|
|
75
|
+
#log;
|
|
76
|
+
// One entry per registered connection, pending or admitted, in
|
|
77
|
+
// registration order (Map iteration is insertion order — the cap's
|
|
78
|
+
// "oldest"). Adding a new identity scope (e.g. `device_id`) means adding a
|
|
79
|
+
// field to `ConnectionIdentity`, not a parallel map.
|
|
19
80
|
#connections = new Map();
|
|
20
|
-
// Reverse map to find connection ID by socket
|
|
21
|
-
#connection_ids = new WeakMap();
|
|
22
|
-
// Auth identity per connection. Adding a new identity scope (e.g.
|
|
23
|
-
// `device_id`) means adding a field here, not a new parallel map.
|
|
24
|
-
#connection_identities = new Map();
|
|
25
81
|
// Server→client request correlation (ActionPeer). The transport owns the
|
|
26
82
|
// sockets + the send; the registry owns the pending map, id allocation,
|
|
27
83
|
// deadlines, and the per-connection in-flight cap (see `peer_request.ts`).
|
|
28
84
|
#pending = new PendingPeerRequests();
|
|
29
85
|
/**
|
|
30
|
-
*
|
|
86
|
+
* @throws Error when `max_connections_per_account` is neither `null` nor a
|
|
87
|
+
* positive integer
|
|
88
|
+
*/
|
|
89
|
+
constructor(options) {
|
|
90
|
+
const max = options?.max_connections_per_account === undefined
|
|
91
|
+
? DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT
|
|
92
|
+
: options.max_connections_per_account;
|
|
93
|
+
if (max !== null && (!Number.isInteger(max) || max < 1)) {
|
|
94
|
+
throw new Error(`BackendWebsocketTransport: max_connections_per_account must be a positive integer or null, got ${max}`);
|
|
95
|
+
}
|
|
96
|
+
this.#max_connections_per_account = max;
|
|
97
|
+
this.#log = options?.log === undefined ? new Logger('[ws]') : options.log;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Register a connection **un-admitted** — phase one of the two-phase
|
|
101
|
+
* registration an upgrade runs (module doc).
|
|
102
|
+
*
|
|
103
|
+
* The entry is closeable at once: a `close_sockets_for_*` matching
|
|
104
|
+
* `token_hash` / `account_id` / `api_token_id` removes it, aborts
|
|
105
|
+
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED`; the
|
|
106
|
+
* later `admit` then refuses. Until admitted it evicts nothing, is not
|
|
107
|
+
* counted toward the per-account cap, and receives no message.
|
|
108
|
+
*
|
|
109
|
+
* The caller owns the entry until it is admitted: every refusal path calls
|
|
110
|
+
* `remove_connection` with the returned id.
|
|
111
|
+
*
|
|
112
|
+
* @param ws - the socket to close on revocation
|
|
113
|
+
* @param token_hash - blake3 session token hash, or `null` for non-session credentials
|
|
114
|
+
* @param account_id - the authenticated account
|
|
115
|
+
* @param api_token_id - `api_token.id` for bearer connections, else `null`
|
|
116
|
+
* @param abort_controller - the socket-scoped controller to abort when the
|
|
117
|
+
* transport closes this connection (revocation or cap eviction)
|
|
118
|
+
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
119
|
+
* @mutates this - inserts an un-admitted entry into `#connections`
|
|
120
|
+
*/
|
|
121
|
+
register_pending(ws, token_hash, account_id, api_token_id = null, abort_controller = null) {
|
|
122
|
+
return this.#insert(ws, token_hash, account_id, api_token_id, abort_controller, false);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Admit a pending connection — phase two (module doc).
|
|
126
|
+
*
|
|
127
|
+
* If the entry is gone — a revocation closed it while it was pending, or it
|
|
128
|
+
* was removed — nothing is evicted and the connection is refused. Otherwise
|
|
129
|
+
* the per-account cap is enforced exactly as in `add_connection`, counting
|
|
130
|
+
* only **admitted** connections, and the entry becomes admitted: it now
|
|
131
|
+
* receives messages, is counted, and can itself be superseded. Admitting an
|
|
132
|
+
* already-admitted connection is a no-op that answers `true`.
|
|
133
|
+
*
|
|
134
|
+
* @param connection_id - the id `register_pending` returned
|
|
135
|
+
* @returns `false` when the connection is no longer registered — the caller
|
|
136
|
+
* closes the socket as revoked
|
|
137
|
+
* @mutates this - marks the entry admitted; past the cap, first removes the
|
|
138
|
+
* account's oldest admitted connections and closes their `WSContext`
|
|
139
|
+
*/
|
|
140
|
+
admit(connection_id) {
|
|
141
|
+
const entry = this.#connections.get(connection_id);
|
|
142
|
+
if (!entry)
|
|
143
|
+
return false;
|
|
144
|
+
if (entry.admitted)
|
|
145
|
+
return true;
|
|
146
|
+
// the entry itself is un-admitted, so the eviction neither counts nor
|
|
147
|
+
// removes it
|
|
148
|
+
if (this.#max_connections_per_account !== null) {
|
|
149
|
+
this.#evict_oldest_for_account(entry.identity.account_id, this.#max_connections_per_account);
|
|
150
|
+
}
|
|
151
|
+
entry.admitted = true;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Register a connection, admitted at once — the one-step form of
|
|
156
|
+
* `register_pending` + `admit`.
|
|
157
|
+
*
|
|
158
|
+
* **Not for an upgrade that authenticated a revocable credential**: a
|
|
159
|
+
* revocation landing between that upgrade's credential check and this call
|
|
160
|
+
* closes nothing, and the connection would be admitted on a dead credential
|
|
161
|
+
* (module doc, "Two-phase registration"). This is for a connection with
|
|
162
|
+
* nothing to revoke behind it.
|
|
163
|
+
*
|
|
31
164
|
* Session connections pass a token hash for targeted revocation.
|
|
32
165
|
* Bearer token connections (`api_token`) pass the `api_token.id` so the
|
|
33
166
|
* socket can be closed when that specific token is revoked without
|
|
@@ -35,93 +168,179 @@ export class BackendWebsocketTransport {
|
|
|
35
168
|
* pass `null` for both — they're only reachable via
|
|
36
169
|
* `close_sockets_for_account`.
|
|
37
170
|
*
|
|
171
|
+
* Enforces the per-account cap (`max_connections_per_account`): when
|
|
172
|
+
* `account_id` already holds the maximum, its oldest admitted connections are
|
|
173
|
+
* removed and sent a `WS_CLOSE_CONNECTION_LIMIT` close before the new one is
|
|
174
|
+
* inserted. A removed connection gets no further broadcast or peer request,
|
|
175
|
+
* stops counting toward the cap at once, and has its `abort_controller`
|
|
176
|
+
* aborted; its socket is closed when the close handshake completes. The new
|
|
177
|
+
* connection is always admitted.
|
|
178
|
+
*
|
|
38
179
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
39
|
-
* @mutates this - inserts into `#connections
|
|
40
|
-
*
|
|
180
|
+
* @mutates this - inserts an admitted entry into `#connections`; past the
|
|
181
|
+
* cap, first removes the account's oldest admitted connections and closes
|
|
182
|
+
* their `WSContext`
|
|
41
183
|
*/
|
|
42
|
-
add_connection(ws, token_hash, account_id, api_token_id = null) {
|
|
184
|
+
add_connection(ws, token_hash, account_id, api_token_id = null, abort_controller = null) {
|
|
185
|
+
if (this.#max_connections_per_account !== null) {
|
|
186
|
+
this.#evict_oldest_for_account(account_id, this.#max_connections_per_account);
|
|
187
|
+
}
|
|
188
|
+
return this.#insert(ws, token_hash, account_id, api_token_id, abort_controller, true);
|
|
189
|
+
}
|
|
190
|
+
#insert(ws, token_hash, account_id, api_token_id, abort_controller, admitted) {
|
|
43
191
|
const connection_id = create_uuid();
|
|
44
|
-
this.#connections.set(connection_id,
|
|
45
|
-
|
|
46
|
-
|
|
192
|
+
this.#connections.set(connection_id, {
|
|
193
|
+
ws,
|
|
194
|
+
identity: { token_hash, account_id, api_token_id },
|
|
195
|
+
admitted,
|
|
196
|
+
abort_controller
|
|
197
|
+
});
|
|
47
198
|
return connection_id;
|
|
48
199
|
}
|
|
49
200
|
/**
|
|
50
|
-
* Remove a
|
|
51
|
-
*
|
|
201
|
+
* Remove a connection and its auth tracking data, pending or admitted, by
|
|
202
|
+
* the id `register_pending` / `add_connection` returned. Idempotent — safe
|
|
203
|
+
* to call after revocation or a cap eviction has already cleaned up, and a
|
|
204
|
+
* no-op for an unknown id.
|
|
205
|
+
*
|
|
206
|
+
* Keyed by id rather than by `WSContext` because a runtime adapter may hand
|
|
207
|
+
* each socket event its own context object (`hono/bun` does), so the one a
|
|
208
|
+
* close event carries can't identify the connection.
|
|
209
|
+
*
|
|
210
|
+
* Removal only: the socket is not closed and the registered
|
|
211
|
+
* `abort_controller` is not aborted — the caller that ends a socket does
|
|
212
|
+
* both.
|
|
52
213
|
*
|
|
53
|
-
* @mutates this - deletes the connection's
|
|
54
|
-
*
|
|
214
|
+
* @mutates this - deletes the connection's entry from `#connections` and
|
|
215
|
+
* settles its pending peer requests as `connection_gone`
|
|
55
216
|
*/
|
|
56
|
-
remove_connection(
|
|
57
|
-
|
|
58
|
-
if (connection_id) {
|
|
59
|
-
this.#cleanup_connection(connection_id, ws);
|
|
60
|
-
}
|
|
217
|
+
remove_connection(connection_id) {
|
|
218
|
+
this.#cleanup_connection(connection_id);
|
|
61
219
|
}
|
|
62
220
|
/**
|
|
63
|
-
* Close every connection whose identity matches the predicate
|
|
221
|
+
* Close every connection whose identity matches the predicate — pending or
|
|
222
|
+
* admitted. Never filter on admission here: reaching a registration in
|
|
223
|
+
* flight is the point of the pending phase, and `admit` then refuses it.
|
|
224
|
+
*
|
|
225
|
+
* A socket whose `close` throws does not stop the loop: every match is
|
|
226
|
+
* removed and closed, and the first error is thrown afterward. The failing
|
|
227
|
+
* socket's connection is already removed and its handlers aborted, so
|
|
228
|
+
* nothing more is dispatched or delivered on it.
|
|
64
229
|
*
|
|
65
230
|
* @returns the number of sockets closed
|
|
231
|
+
* @throws the first error a socket's `close` threw, after every match was attempted
|
|
66
232
|
*/
|
|
67
233
|
#close_where(predicate) {
|
|
68
234
|
let count = 0;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
235
|
+
let failed = false;
|
|
236
|
+
let first_error;
|
|
237
|
+
// deleting the current entry during Map iteration is safe
|
|
238
|
+
for (const [connection_id, entry] of this.#connections) {
|
|
239
|
+
if (predicate(entry.identity)) {
|
|
240
|
+
try {
|
|
241
|
+
this.#revoke_connection(connection_id, entry);
|
|
74
242
|
count++;
|
|
75
243
|
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (!failed) {
|
|
246
|
+
failed = true;
|
|
247
|
+
first_error = error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
76
250
|
}
|
|
77
251
|
}
|
|
252
|
+
if (failed)
|
|
253
|
+
throw first_error;
|
|
78
254
|
return count;
|
|
79
255
|
}
|
|
80
256
|
/**
|
|
81
|
-
* Close all sockets associated with a specific session token hash
|
|
257
|
+
* Close all sockets associated with a specific session token hash,
|
|
258
|
+
* including a registration still pending admission.
|
|
82
259
|
*
|
|
83
260
|
* @returns the number of sockets closed
|
|
84
|
-
* @mutates this - removes matching connections
|
|
85
|
-
* closes their underlying `WSContext` with
|
|
261
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
262
|
+
* controllers, and closes their underlying `WSContext` with
|
|
263
|
+
* `WS_CLOSE_SESSION_REVOKED`
|
|
86
264
|
*/
|
|
87
265
|
close_sockets_for_session(token_hash) {
|
|
88
266
|
return this.#close_where((id) => id.token_hash === token_hash);
|
|
89
267
|
}
|
|
90
268
|
/**
|
|
91
|
-
* Close all sockets associated with a specific account
|
|
269
|
+
* Close all sockets associated with a specific account, including a
|
|
270
|
+
* registration still pending admission.
|
|
92
271
|
*
|
|
93
272
|
* @returns the number of sockets closed
|
|
94
|
-
* @mutates this - removes matching connections
|
|
95
|
-
* closes their underlying `WSContext` with
|
|
273
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
274
|
+
* controllers, and closes their underlying `WSContext` with
|
|
275
|
+
* `WS_CLOSE_SESSION_REVOKED`
|
|
96
276
|
*/
|
|
97
277
|
close_sockets_for_account(account_id) {
|
|
98
278
|
return this.#close_where((id) => id.account_id === account_id);
|
|
99
279
|
}
|
|
100
280
|
/**
|
|
101
|
-
* Close all sockets associated with a specific API token
|
|
281
|
+
* Close all sockets associated with a specific API token, including a
|
|
282
|
+
* registration still pending admission.
|
|
102
283
|
*
|
|
103
|
-
*
|
|
104
|
-
* tear down the account's
|
|
105
|
-
* tokens' sockets.
|
|
284
|
+
* Closes just this token's sockets, so revoking one token — or evicting
|
|
285
|
+
* one past the token cap — doesn't tear down the account's
|
|
286
|
+
* session-authenticated sockets or other tokens' sockets.
|
|
106
287
|
*
|
|
107
288
|
* @returns the number of sockets closed
|
|
108
|
-
* @mutates this - removes matching connections
|
|
109
|
-
* closes their underlying `WSContext` with
|
|
289
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
290
|
+
* controllers, and closes their underlying `WSContext` with
|
|
291
|
+
* `WS_CLOSE_SESSION_REVOKED`
|
|
110
292
|
*/
|
|
111
293
|
close_sockets_for_token(api_token_id) {
|
|
112
294
|
return this.#close_where((id) => id.api_token_id === api_token_id);
|
|
113
295
|
}
|
|
114
|
-
#cleanup_connection(connection_id
|
|
296
|
+
#cleanup_connection(connection_id) {
|
|
115
297
|
this.#connections.delete(connection_id);
|
|
116
|
-
this.#connection_ids.delete(ws);
|
|
117
|
-
this.#connection_identities.delete(connection_id);
|
|
118
298
|
// Wake any handler still awaiting a reply on this socket — the peer is
|
|
119
299
|
// gone, so the request can never complete.
|
|
120
300
|
this.#pending.drain(connection_id);
|
|
121
301
|
}
|
|
122
|
-
#revoke_connection(connection_id,
|
|
123
|
-
this.#cleanup_connection(connection_id
|
|
124
|
-
|
|
302
|
+
#revoke_connection(connection_id, entry) {
|
|
303
|
+
this.#cleanup_connection(connection_id);
|
|
304
|
+
// abort before the close so a handler observing `ctx.signal` bails out
|
|
305
|
+
// without waiting for the close handshake
|
|
306
|
+
entry.abort_controller?.abort();
|
|
307
|
+
entry.ws.close(WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Make room for one more admitted connection on `account_id`: when the
|
|
311
|
+
* account already holds `max`, close its oldest so that `max - 1` remain.
|
|
312
|
+
* Pending registrations are neither counted nor closed — a connection that
|
|
313
|
+
* may yet be refused must not cost a live one its slot.
|
|
314
|
+
*/
|
|
315
|
+
#evict_oldest_for_account(account_id, max) {
|
|
316
|
+
// Map iteration is insertion order, so `owned` is oldest-first
|
|
317
|
+
const owned = [];
|
|
318
|
+
for (const [connection_id, entry] of this.#connections) {
|
|
319
|
+
if (entry.admitted && entry.identity.account_id === account_id) {
|
|
320
|
+
owned.push([connection_id, entry]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const excess = owned.length + 1 - max;
|
|
324
|
+
if (excess <= 0)
|
|
325
|
+
return;
|
|
326
|
+
for (let i = 0; i < excess; i++) {
|
|
327
|
+
const [connection_id, entry] = owned[i];
|
|
328
|
+
this.#supersede_connection(connection_id, entry);
|
|
329
|
+
}
|
|
330
|
+
// same line as the Rust spine's registry, so one grep finds either
|
|
331
|
+
this.#log?.info('ws: connection cap closed oldest', { account_id, closed: excess, max });
|
|
332
|
+
}
|
|
333
|
+
#supersede_connection(connection_id, entry) {
|
|
334
|
+
this.#cleanup_connection(connection_id);
|
|
335
|
+
try {
|
|
336
|
+
entry.abort_controller?.abort();
|
|
337
|
+
entry.ws.close(WS_CLOSE_CONNECTION_LIMIT, 'connection limit');
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
// the evicted socket is another connection's — a failure closing it
|
|
341
|
+
// must not fail the connection being admitted
|
|
342
|
+
this.#log?.error('error closing superseded client:', error);
|
|
343
|
+
}
|
|
125
344
|
}
|
|
126
345
|
async send(message, _options) {
|
|
127
346
|
if (is_jsonrpc_request(message)) {
|
|
@@ -138,19 +357,23 @@ export class BackendWebsocketTransport {
|
|
|
138
357
|
}
|
|
139
358
|
#broadcast(message) {
|
|
140
359
|
const serialized = JSON.stringify(message);
|
|
141
|
-
for (const
|
|
360
|
+
for (const entry of this.#connections.values()) {
|
|
361
|
+
// a registration pending admission is not a connection yet
|
|
362
|
+
if (!entry.admitted)
|
|
363
|
+
continue;
|
|
142
364
|
try {
|
|
143
|
-
ws.send(serialized);
|
|
365
|
+
entry.ws.send(serialized);
|
|
144
366
|
}
|
|
145
367
|
catch (error) {
|
|
146
|
-
|
|
368
|
+
this.#log?.error('error broadcasting to client:', error);
|
|
147
369
|
}
|
|
148
370
|
}
|
|
149
371
|
// TODO hack - remove if not ever needed, I assume this will need to be async so let's hold that assumption
|
|
150
372
|
return Promise.resolve();
|
|
151
373
|
}
|
|
152
374
|
/**
|
|
153
|
-
* Broadcast to connections whose identity satisfies a predicate.
|
|
375
|
+
* Broadcast to admitted connections whose identity satisfies a predicate. A
|
|
376
|
+
* registration pending admission is skipped before the predicate runs.
|
|
154
377
|
*
|
|
155
378
|
* Used by the broadcast API when a consumer supplies a subscription ACL hook
|
|
156
379
|
* (e.g. zap's `zap_run_created` only reaches the account that owns the run).
|
|
@@ -162,24 +385,21 @@ export class BackendWebsocketTransport {
|
|
|
162
385
|
broadcast_filtered(message, predicate) {
|
|
163
386
|
const serialized = JSON.stringify(message);
|
|
164
387
|
let count = 0;
|
|
165
|
-
for (const
|
|
166
|
-
if (!predicate(identity))
|
|
167
|
-
continue;
|
|
168
|
-
const ws = this.#connections.get(connection_id);
|
|
169
|
-
if (!ws)
|
|
388
|
+
for (const entry of this.#connections.values()) {
|
|
389
|
+
if (!entry.admitted || !predicate(entry.identity))
|
|
170
390
|
continue;
|
|
171
391
|
try {
|
|
172
|
-
ws.send(serialized);
|
|
392
|
+
entry.ws.send(serialized);
|
|
173
393
|
count++;
|
|
174
394
|
}
|
|
175
395
|
catch (error) {
|
|
176
|
-
|
|
396
|
+
this.#log?.error('error broadcasting filtered to client:', error);
|
|
177
397
|
}
|
|
178
398
|
}
|
|
179
399
|
return count;
|
|
180
400
|
}
|
|
181
401
|
/**
|
|
182
|
-
* Send a message to every socket bound to a specific account.
|
|
402
|
+
* Send a message to every admitted socket bound to a specific account.
|
|
183
403
|
*
|
|
184
404
|
* Targeted per-account fan-out for any flow where the delivery target
|
|
185
405
|
* is a single known account. Prefer this over `broadcast_filtered` when
|
|
@@ -204,7 +424,9 @@ export class BackendWebsocketTransport {
|
|
|
204
424
|
* connection. Resolves when the client's matching reply arrives (routed in
|
|
205
425
|
* via `resolve_peer_response`), the deadline elapses (`timeout`), the
|
|
206
426
|
* per-connection cap is hit (`too_many_in_flight`), or the socket closes
|
|
207
|
-
* (`connection_gone`).
|
|
427
|
+
* (`connection_gone`). A connection still pending admission answers
|
|
428
|
+
* `connection_gone` like an unknown one. Never throws — every failure is a
|
|
429
|
+
* `PeerRequestError`.
|
|
208
430
|
*
|
|
209
431
|
* Delegates correlation to `#pending` (id allocation, deadline, cap, drain);
|
|
210
432
|
* this method owns only the socket lookup + the send. Server-issued ids are
|
|
@@ -215,9 +437,11 @@ export class BackendWebsocketTransport {
|
|
|
215
437
|
* @mutates this - registers then clears an entry in `#pending`
|
|
216
438
|
*/
|
|
217
439
|
request_connection(connection_id, method, params, options) {
|
|
218
|
-
const
|
|
219
|
-
if (!
|
|
440
|
+
const entry = this.#connections.get(connection_id);
|
|
441
|
+
if (!entry?.admitted) {
|
|
220
442
|
return Promise.resolve({ ok: false, error: { kind: 'connection_gone' } });
|
|
443
|
+
}
|
|
444
|
+
const { ws } = entry;
|
|
221
445
|
const registered = this.#pending.register(connection_id, options?.timeout_ms);
|
|
222
446
|
if (!registered)
|
|
223
447
|
return Promise.resolve({ ok: false, error: { kind: 'too_many_in_flight' } });
|
|
@@ -246,18 +470,51 @@ export class BackendWebsocketTransport {
|
|
|
246
470
|
resolve_peer_response(connection_id, response) {
|
|
247
471
|
return this.#pending.resolve(connection_id, response);
|
|
248
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Whether a connection is still registered — and admitted: a registration
|
|
475
|
+
* pending admission answers `false`, like every other reader of the
|
|
476
|
+
* transport.
|
|
477
|
+
*
|
|
478
|
+
* `register_action_ws` checks it before dispatching each inbound message, so
|
|
479
|
+
* a socket the server has closed — revoked, superseded by the cap, or
|
|
480
|
+
* refused at admission — dispatches nothing more while its close handshake
|
|
481
|
+
* is still in flight. The twin of the Rust `ConnectionRegistry::is_registered`.
|
|
482
|
+
*/
|
|
483
|
+
is_registered(connection_id) {
|
|
484
|
+
return this.#connections.get(connection_id)?.admitted === true;
|
|
485
|
+
}
|
|
249
486
|
is_ready() {
|
|
250
|
-
return this
|
|
487
|
+
return this.get_connection_count() > 0;
|
|
251
488
|
}
|
|
252
489
|
/**
|
|
253
|
-
* Number of currently
|
|
490
|
+
* Number of currently **admitted** WebSocket connections.
|
|
254
491
|
*
|
|
255
|
-
* Read-only counter intended for telemetry, logging, and tests.
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* adapter's
|
|
492
|
+
* Read-only counter intended for telemetry, logging, and tests. A
|
|
493
|
+
* registration still pending admission is not a connection yet and is
|
|
494
|
+
* counted by `get_pending_connection_count` instead. Includes connections
|
|
495
|
+
* that have been closed by the peer but not yet removed by the WS adapter's
|
|
496
|
+
* `onClose` callback.
|
|
259
497
|
*/
|
|
260
498
|
get_connection_count() {
|
|
261
|
-
|
|
499
|
+
let count = 0;
|
|
500
|
+
for (const entry of this.#connections.values()) {
|
|
501
|
+
if (entry.admitted)
|
|
502
|
+
count++;
|
|
503
|
+
}
|
|
504
|
+
return count;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Number of registrations awaiting admission — between `register_pending`
|
|
508
|
+
* and `admit` (or their refusal). Each lives for one credential re-check, so
|
|
509
|
+
* a count that stays high means upgrades are stalling there. For telemetry
|
|
510
|
+
* and tests.
|
|
511
|
+
*/
|
|
512
|
+
get_pending_connection_count() {
|
|
513
|
+
let count = 0;
|
|
514
|
+
for (const entry of this.#connections.values()) {
|
|
515
|
+
if (!entry.admitted)
|
|
516
|
+
count++;
|
|
517
|
+
}
|
|
518
|
+
return count;
|
|
262
519
|
}
|
|
263
520
|
}
|
|
@@ -52,6 +52,14 @@ export interface WsEndpointSpec {
|
|
|
52
52
|
* `AppServer.ws_endpoints[path]` for broadcast / fan-out.
|
|
53
53
|
*/
|
|
54
54
|
transport?: BackendWebsocketTransport;
|
|
55
|
+
/**
|
|
56
|
+
* Per-account connection cap for the auto-created transport. See
|
|
57
|
+
* `RegisterActionWsOptions.max_connections_per_account` — rejected
|
|
58
|
+
* alongside `transport`, which carries its own cap.
|
|
59
|
+
*
|
|
60
|
+
* @default DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT
|
|
61
|
+
*/
|
|
62
|
+
max_connections_per_account?: number | null;
|
|
55
63
|
/**
|
|
56
64
|
* Server-side heartbeat policy. Default-on (60s receive-silence
|
|
57
65
|
* timeout). Set `false` only when an upstream stack (TCP keepalive,
|
|
@@ -68,23 +76,29 @@ export interface WsEndpointSpec {
|
|
|
68
76
|
*/
|
|
69
77
|
max_message_bytes?: number;
|
|
70
78
|
/**
|
|
71
|
-
* Called once per socket after
|
|
72
|
-
* the first message
|
|
73
|
-
* `RegisterActionWsOptions.on_socket_open`.
|
|
79
|
+
* Called once per socket after the connection is admitted — registered
|
|
80
|
+
* and its credential re-checked — and before the first message
|
|
81
|
+
* dispatches. See `RegisterActionWsOptions.on_socket_open`.
|
|
74
82
|
*/
|
|
75
83
|
on_socket_open?: (ctx: SocketOpenContext) => void | Promise<void>;
|
|
76
84
|
/**
|
|
77
|
-
* Called once per socket on close,
|
|
78
|
-
*
|
|
85
|
+
* Called once per admitted socket on close, after
|
|
86
|
+
* `transport.remove_connection`. See
|
|
87
|
+
* `RegisterActionWsOptions.on_socket_close`.
|
|
79
88
|
*/
|
|
80
89
|
on_socket_close?: (ctx: SocketCloseContext) => void | Promise<void>;
|
|
81
90
|
/**
|
|
82
|
-
* Default `true` —
|
|
83
|
-
* `
|
|
84
|
-
*
|
|
91
|
+
* Default `true` — registers `create_ws_auth_guard` against this
|
|
92
|
+
* endpoint's transport via `deps.audit.add_listener`, so a revocation's
|
|
93
|
+
* audit row closes its sockets. Wiring is deduped by
|
|
85
94
|
* transport **reference identity** (`WeakSet<BackendWebsocketTransport>`),
|
|
86
95
|
* so two `WsEndpointSpec`s sharing the exact same instance get a
|
|
87
|
-
* single
|
|
96
|
+
* single listener.
|
|
97
|
+
*
|
|
98
|
+
* The guard repeats what the revocation handlers already do: the
|
|
99
|
+
* transport is always in `deps.connection_closer`, whatever this flag
|
|
100
|
+
* says, and the handlers close through it after their commit. Turning the
|
|
101
|
+
* guard off leaves those closes in place.
|
|
88
102
|
*
|
|
89
103
|
* **Shared-transport OR-semantics.** When multiple `WsEndpointSpec`s
|
|
90
104
|
* share one transport, the guard is wired iff **any** of those specs
|
|
@@ -96,11 +110,10 @@ export interface WsEndpointSpec {
|
|
|
96
110
|
* Reference-identity dedupe means **wrapped or proxied transports
|
|
97
111
|
* dedupe as separate entries** — a consumer threading every
|
|
98
112
|
* transport through a tracing / DI / metrics shim will get a fresh
|
|
99
|
-
*
|
|
113
|
+
* listener per shimmed reference, even when the underlying
|
|
100
114
|
* transport is the same. If you wrap or proxy, set `auth_guard:
|
|
101
115
|
* false` on the duplicate `WsEndpointSpec`s and compose
|
|
102
|
-
* `create_ws_auth_guard`
|
|
103
|
-
* underlying transport once.
|
|
116
|
+
* `create_ws_auth_guard` against the underlying transport once.
|
|
104
117
|
*
|
|
105
118
|
* Set `false` when a consumer needs to compose their own callback
|
|
106
119
|
* from scratch — or to opt out of the auto-wiring entirely.
|
|
@@ -115,7 +128,7 @@ export interface WsEndpointSpec {
|
|
|
115
128
|
/**
|
|
116
129
|
* Extra audit-event handlers registered via `deps.audit.add_listener`
|
|
117
130
|
* AFTER the standard `auth_guard` wiring (when enabled). By the time
|
|
118
|
-
* these run, the standard
|
|
131
|
+
* these run, the standard guard may have already closed sockets. Use
|
|
119
132
|
* for role-revoke disconnection, custom analytics, etc.
|
|
120
133
|
*
|
|
121
134
|
* Never deduped — consumer-owned; pass the same handler twice and it
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_endpoint_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/ws_endpoint_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC9B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzC;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE
|
|
1
|
+
{"version":3,"file":"ws_endpoint_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/ws_endpoint_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC9B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzC;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;;OAMG;IACH,2BAA2B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;OAQG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CACxD"}
|