@fuzdev/fuz_app 0.118.0 → 0.120.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 +31 -3
- 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 +19 -8
- package/dist/db/fact_disk_storage.d.ts +10 -8
- package/dist/db/fact_disk_storage.d.ts.map +1 -1
- package/dist/db/fact_disk_storage.js +13 -17
- package/dist/db/fact_store.d.ts +39 -38
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/fact_store.js +48 -86
- package/dist/db/file_fact_url.d.ts +6 -6
- package/dist/db/file_fact_url.js +6 -6
- 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/server/serve_fact_route.d.ts +10 -7
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +11 -8
- 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 +2 -2
- package/dist/server/file_fact_fetcher.d.ts +0 -42
- package/dist/server/file_fact_fetcher.d.ts.map +0 -1
- package/dist/server/file_fact_fetcher.js +0 -60
|
@@ -59,6 +59,7 @@ export interface RegisterWsEndpointOptions extends RegisterActionWsOptions {
|
|
|
59
59
|
* @mutates options.app - applies origin/auth/scope/authorization/role middleware
|
|
60
60
|
* via `app.use`, then registers the `GET path` route via the inner
|
|
61
61
|
* `register_action_ws`
|
|
62
|
+
* @mutates options.connection_closer - adds the endpoint's transport
|
|
62
63
|
*/
|
|
63
64
|
export declare const register_ws_endpoint: (options: RegisterWsEndpointOptions) => RegisterActionWsResult;
|
|
64
65
|
//# sourceMappingURL=register_ws_endpoint.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAaH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,0CAA0C;AAC1C,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACzE;;;;OAIG;IACH,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC;AAgDD
|
|
1
|
+
{"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAaH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,0CAA0C;AAC1C,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACzE;;;;OAIG;IACH,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC;AAgDD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,yBAAyB,KAChC,sBAoBF,CAAC"}
|
|
@@ -83,6 +83,7 @@ const create_ws_authorization_middleware = (db) => {
|
|
|
83
83
|
* @mutates options.app - applies origin/auth/scope/authorization/role middleware
|
|
84
84
|
* via `app.use`, then registers the `GET path` route via the inner
|
|
85
85
|
* `register_action_ws`
|
|
86
|
+
* @mutates options.connection_closer - adds the endpoint's transport
|
|
86
87
|
*/
|
|
87
88
|
export const register_ws_endpoint = (options) => {
|
|
88
89
|
const { app, path, allowed_origins, db, required_roles, log = new Logger('[ws]'), ...rest } = options;
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* Drop into any SvelteKit frontend as the underlying connection for
|
|
6
6
|
* `FrontendWebsocketTransport`. Handles auto-reconnect with exponential
|
|
7
7
|
* backoff, respects `WS_CLOSE_SESSION_REVOKED` (no reconnect loop after the
|
|
8
|
-
* server revokes auth)
|
|
9
|
-
*
|
|
8
|
+
* server revokes auth) and `WS_CLOSE_CONNECTION_LIMIT` (no reconnect after the
|
|
9
|
+
* server closes this socket to admit a newer one — closed until the user
|
|
10
|
+
* acts), exposes reactive status for UI indicators, and ships three
|
|
11
|
+
* correctness primitives default-on:
|
|
10
12
|
*
|
|
11
13
|
* - `FrontendWebsocketClient.request` — promise-based JSON-RPC with
|
|
12
14
|
* auto-assigned ids and a pending-id map. Intercepts responses on the
|
|
@@ -72,8 +74,9 @@ export declare const DEFAULT_QUEUE_MAX_SIZE = 100;
|
|
|
72
74
|
* - `connecting` — WebSocket `readyState === CONNECTING`.
|
|
73
75
|
* - `connected` — WebSocket `readyState === OPEN`.
|
|
74
76
|
* - `reconnecting` — close fired; waiting out backoff before next attempt.
|
|
75
|
-
* - `closed` — socket is not open
|
|
76
|
-
*
|
|
77
|
+
* - `closed` — socket is not open, and nothing is scheduled to reopen it.
|
|
78
|
+
* Terminal only when `revoked` is `true`; otherwise `connect()` reopens,
|
|
79
|
+
* including after a `superseded` close.
|
|
77
80
|
*/
|
|
78
81
|
export type SocketStatus = 'initial' | 'connecting' | 'connected' | 'reconnecting' | 'closed';
|
|
79
82
|
export type SocketMessageHandler = (event: MessageEvent) => void;
|
|
@@ -145,6 +148,11 @@ export interface FrontendWebsocketClientOptions {
|
|
|
145
148
|
*
|
|
146
149
|
* Session-revocation close codes (`WS_CLOSE_SESSION_REVOKED`) put the client
|
|
147
150
|
* in a permanently-closed state; reconnecting would just loop on 401.
|
|
151
|
+
*
|
|
152
|
+
* A connection-limit close (`WS_CLOSE_CONNECTION_LIMIT`) leaves the client
|
|
153
|
+
* closed with `superseded` set and no reconnect scheduled — the server closed
|
|
154
|
+
* this socket to admit a newer one on the same account, and reconnecting
|
|
155
|
+
* would close that one in turn. An explicit `connect()` reopens.
|
|
148
156
|
*/
|
|
149
157
|
export declare class FrontendWebsocketClient implements WebsocketConnection, Disposable {
|
|
150
158
|
#private;
|
|
@@ -247,14 +255,35 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
|
|
|
247
255
|
* reflects any closed state (incl. user-initiated `disconnect()`).
|
|
248
256
|
*/
|
|
249
257
|
get revoked(): boolean;
|
|
258
|
+
/**
|
|
259
|
+
* Whether the server closed this client's socket to admit a newer
|
|
260
|
+
* connection on the same account (`WS_CLOSE_CONNECTION_LIMIT`) and it
|
|
261
|
+
* hasn't been reopened since. While `true` the client is `closed` and
|
|
262
|
+
* schedules no reconnect — reconnecting on its own would supersede a newer
|
|
263
|
+
* socket in turn, and clients past the cap would close each other in a loop.
|
|
264
|
+
*
|
|
265
|
+
* Unlike `revoked` this is not terminal: the credential is still good, and
|
|
266
|
+
* the next `connect()` clears it and reopens — only `connect()` does;
|
|
267
|
+
* `disconnect()` leaves it set. Requests in flight or queued at the close
|
|
268
|
+
* reject with `service_unavailable`; ones made afterwards buffer in the
|
|
269
|
+
* durable queue like any other call on a closed client. Reactive, so UI can
|
|
270
|
+
* say why the connection is closed and offer the reconnect.
|
|
271
|
+
*
|
|
272
|
+
* `socket_status_to_async_status` reads a superseded client as `initial`,
|
|
273
|
+
* the same as one that was never connected. An app that connects on its
|
|
274
|
+
* own when its status reads `initial` must also check `superseded`, or it
|
|
275
|
+
* rebuilds the loop.
|
|
276
|
+
*/
|
|
277
|
+
get superseded(): boolean;
|
|
250
278
|
/**
|
|
251
279
|
* Open the WebSocket. No-op on SSR, or if the session has been revoked.
|
|
252
280
|
* Cancels any pending reconnect and tears down any existing connection first;
|
|
253
|
-
* an open prior socket is closed with a normal-closure code.
|
|
281
|
+
* an open prior socket is closed with a normal-closure code. Reopens after
|
|
282
|
+
* a `superseded` close, clearing the flag.
|
|
254
283
|
*
|
|
255
|
-
* @mutates this - replaces `ws`, sets `status` to
|
|
256
|
-
* `closed` on construction failure), and on construction
|
|
257
|
-
* schedule a reconnect (mutating `reconnect_count` /
|
|
284
|
+
* @mutates this - replaces `ws`, clears `superseded`, sets `status` to
|
|
285
|
+
* `connecting` (or `closed` on construction failure), and on construction
|
|
286
|
+
* failure may schedule a reconnect (mutating `reconnect_count` /
|
|
258
287
|
* `current_reconnect_delay`)
|
|
259
288
|
*/
|
|
260
289
|
connect(): void;
|
|
@@ -309,7 +338,7 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
|
|
|
309
338
|
* - `request_cancelled` — caller's `AbortSignal` fired
|
|
310
339
|
* - `queue_overflow` — durable queue full
|
|
311
340
|
* - `service_unavailable` — socket not connected / closed / torn down
|
|
312
|
-
* mid-flight
|
|
341
|
+
* mid-flight, including a `superseded` close
|
|
313
342
|
* - `internal_error` — `ws.send` threw (serialization, buffer full)
|
|
314
343
|
* - server's wire code verbatim — JSON-RPC error frame from peer
|
|
315
344
|
*/
|
|
@@ -328,6 +357,11 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
|
|
|
328
357
|
* `failure` (UI shows "lost, retrying") and splits `closed` by `revoked` so
|
|
329
358
|
* a terminal session-revocation read as `failure` while a clean client-
|
|
330
359
|
* initiated close reads as `initial` (the "not connected, not trying" state).
|
|
360
|
+
* A `superseded` close reads as `initial` too — the client is not trying and
|
|
361
|
+
* an explicit `connect()` reopens it; read `FrontendWebsocketClient.superseded`
|
|
362
|
+
* to tell the user why, and before connecting on an `initial` status without
|
|
363
|
+
* the user asking (connecting a superseded client on its own closes a newer
|
|
364
|
+
* socket in turn, which is the loop the close code exists to stop).
|
|
331
365
|
*
|
|
332
366
|
* @param revoked - whether the session has been permanently revoked
|
|
333
367
|
* (typically `FrontendWebsocketClient.revoked`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* Drop into any SvelteKit frontend as the underlying connection for
|
|
6
6
|
* `FrontendWebsocketTransport`. Handles auto-reconnect with exponential
|
|
7
7
|
* backoff, respects `WS_CLOSE_SESSION_REVOKED` (no reconnect loop after the
|
|
8
|
-
* server revokes auth)
|
|
9
|
-
*
|
|
8
|
+
* server revokes auth) and `WS_CLOSE_CONNECTION_LIMIT` (no reconnect after the
|
|
9
|
+
* server closes this socket to admit a newer one — closed until the user
|
|
10
|
+
* acts), exposes reactive status for UI indicators, and ships three
|
|
11
|
+
* correctness primitives default-on:
|
|
10
12
|
*
|
|
11
13
|
* - `FrontendWebsocketClient.request` — promise-based JSON-RPC with
|
|
12
14
|
* auto-assigned ids and a pending-id map. Intercepts responses on the
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
import { BROWSER } from 'esm-env';
|
|
28
30
|
import { JSONRPC_VERSION } from '../http/jsonrpc.js';
|
|
29
31
|
import { JSONRPC_ERROR_CODES, ThrownJsonrpcError, jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
30
|
-
import { WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT, WS_CLOSE_SESSION_REVOKED } from './transports.js';
|
|
32
|
+
import { WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT, WS_CLOSE_CONNECTION_LIMIT, WS_CLOSE_SESSION_REVOKED } from './transports.js';
|
|
31
33
|
import { cancel_action_spec } from './cancel.js';
|
|
32
34
|
import { heartbeat_action_spec } from './heartbeat.js';
|
|
33
35
|
/** Default WebSocket close code (normal closure). */
|
|
@@ -77,6 +79,11 @@ export const DEFAULT_QUEUE_MAX_SIZE = 100;
|
|
|
77
79
|
*
|
|
78
80
|
* Session-revocation close codes (`WS_CLOSE_SESSION_REVOKED`) put the client
|
|
79
81
|
* in a permanently-closed state; reconnecting would just loop on 401.
|
|
82
|
+
*
|
|
83
|
+
* A connection-limit close (`WS_CLOSE_CONNECTION_LIMIT`) leaves the client
|
|
84
|
+
* closed with `superseded` set and no reconnect scheduled — the server closed
|
|
85
|
+
* this socket to admit a newer one on the same account, and reconnecting
|
|
86
|
+
* would close that one in turn. An explicit `connect()` reopens.
|
|
80
87
|
*/
|
|
81
88
|
export class FrontendWebsocketClient {
|
|
82
89
|
#url;
|
|
@@ -136,6 +143,7 @@ export class FrontendWebsocketClient {
|
|
|
136
143
|
#reconnect_timeout = null;
|
|
137
144
|
#reconnect_scheduled_at = null;
|
|
138
145
|
#revoked = $state.raw(false);
|
|
146
|
+
#superseded = $state.raw(false);
|
|
139
147
|
#message_handlers = new Set();
|
|
140
148
|
#error_handlers = new Set();
|
|
141
149
|
connected = $derived(this.status === 'connected');
|
|
@@ -277,14 +285,37 @@ export class FrontendWebsocketClient {
|
|
|
277
285
|
get revoked() {
|
|
278
286
|
return this.#revoked;
|
|
279
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Whether the server closed this client's socket to admit a newer
|
|
290
|
+
* connection on the same account (`WS_CLOSE_CONNECTION_LIMIT`) and it
|
|
291
|
+
* hasn't been reopened since. While `true` the client is `closed` and
|
|
292
|
+
* schedules no reconnect — reconnecting on its own would supersede a newer
|
|
293
|
+
* socket in turn, and clients past the cap would close each other in a loop.
|
|
294
|
+
*
|
|
295
|
+
* Unlike `revoked` this is not terminal: the credential is still good, and
|
|
296
|
+
* the next `connect()` clears it and reopens — only `connect()` does;
|
|
297
|
+
* `disconnect()` leaves it set. Requests in flight or queued at the close
|
|
298
|
+
* reject with `service_unavailable`; ones made afterwards buffer in the
|
|
299
|
+
* durable queue like any other call on a closed client. Reactive, so UI can
|
|
300
|
+
* say why the connection is closed and offer the reconnect.
|
|
301
|
+
*
|
|
302
|
+
* `socket_status_to_async_status` reads a superseded client as `initial`,
|
|
303
|
+
* the same as one that was never connected. An app that connects on its
|
|
304
|
+
* own when its status reads `initial` must also check `superseded`, or it
|
|
305
|
+
* rebuilds the loop.
|
|
306
|
+
*/
|
|
307
|
+
get superseded() {
|
|
308
|
+
return this.#superseded;
|
|
309
|
+
}
|
|
280
310
|
/**
|
|
281
311
|
* Open the WebSocket. No-op on SSR, or if the session has been revoked.
|
|
282
312
|
* Cancels any pending reconnect and tears down any existing connection first;
|
|
283
|
-
* an open prior socket is closed with a normal-closure code.
|
|
313
|
+
* an open prior socket is closed with a normal-closure code. Reopens after
|
|
314
|
+
* a `superseded` close, clearing the flag.
|
|
284
315
|
*
|
|
285
|
-
* @mutates this - replaces `ws`, sets `status` to
|
|
286
|
-
* `closed` on construction failure), and on construction
|
|
287
|
-
* schedule a reconnect (mutating `reconnect_count` /
|
|
316
|
+
* @mutates this - replaces `ws`, clears `superseded`, sets `status` to
|
|
317
|
+
* `connecting` (or `closed` on construction failure), and on construction
|
|
318
|
+
* failure may schedule a reconnect (mutating `reconnect_count` /
|
|
288
319
|
* `current_reconnect_delay`)
|
|
289
320
|
*/
|
|
290
321
|
connect() {
|
|
@@ -292,6 +323,7 @@ export class FrontendWebsocketClient {
|
|
|
292
323
|
return;
|
|
293
324
|
if (this.#revoked)
|
|
294
325
|
return;
|
|
326
|
+
this.#superseded = false;
|
|
295
327
|
this.#cancel_reconnect();
|
|
296
328
|
this.#teardown(DEFAULT_CLOSE_CODE);
|
|
297
329
|
try {
|
|
@@ -390,7 +422,7 @@ export class FrontendWebsocketClient {
|
|
|
390
422
|
* - `request_cancelled` — caller's `AbortSignal` fired
|
|
391
423
|
* - `queue_overflow` — durable queue full
|
|
392
424
|
* - `service_unavailable` — socket not connected / closed / torn down
|
|
393
|
-
* mid-flight
|
|
425
|
+
* mid-flight, including a `superseded` close
|
|
394
426
|
* - `internal_error` — `ws.send` threw (serialization, buffer full)
|
|
395
427
|
* - server's wire code verbatim — JSON-RPC error frame from peer
|
|
396
428
|
*/
|
|
@@ -697,6 +729,20 @@ export class FrontendWebsocketClient {
|
|
|
697
729
|
this.#reject_all('session revoked', jsonrpc_errors.unauthenticated);
|
|
698
730
|
return;
|
|
699
731
|
}
|
|
732
|
+
// The server closed this socket to admit a newer one on the same account.
|
|
733
|
+
// Reconnecting would supersede that one in turn — two clients past the cap
|
|
734
|
+
// would close each other in a loop — so stay closed until `connect()` is
|
|
735
|
+
// called. Not a revocation: the credential is still good. Nothing will
|
|
736
|
+
// reopen the socket on its own, so the queue is rejected with the pending
|
|
737
|
+
// requests, as when auto-reconnect is off.
|
|
738
|
+
if (event.code === WS_CLOSE_CONNECTION_LIMIT) {
|
|
739
|
+
this.#superseded = true;
|
|
740
|
+
this.status = 'closed';
|
|
741
|
+
this.#cancel_reconnect();
|
|
742
|
+
this.#reset_reconnect_counters();
|
|
743
|
+
this.#reject_all('connection superseded', jsonrpc_errors.service_unavailable);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
700
746
|
// Pending in-flight requests can't be correlated post-reconnect; reject
|
|
701
747
|
// them. Queue stays so the flush on reopen replays unsent work.
|
|
702
748
|
this.#reject_pending_only(`connection closed (code ${event.code})`, jsonrpc_errors.service_unavailable);
|
|
@@ -785,6 +831,11 @@ export class FrontendWebsocketClient {
|
|
|
785
831
|
* `failure` (UI shows "lost, retrying") and splits `closed` by `revoked` so
|
|
786
832
|
* a terminal session-revocation read as `failure` while a clean client-
|
|
787
833
|
* initiated close reads as `initial` (the "not connected, not trying" state).
|
|
834
|
+
* A `superseded` close reads as `initial` too — the client is not trying and
|
|
835
|
+
* an explicit `connect()` reopens it; read `FrontendWebsocketClient.superseded`
|
|
836
|
+
* to tell the user why, and before connecting on an `initial` status without
|
|
837
|
+
* the user asking (connecting a superseded client on its own closes a newer
|
|
838
|
+
* socket in turn, which is the loop the close code exists to stop).
|
|
788
839
|
*
|
|
789
840
|
* @param revoked - whether the session has been permanently revoked
|
|
790
841
|
* (typically `FrontendWebsocketClient.revoked`)
|
|
@@ -8,17 +8,64 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import type { JsonrpcMessageFromClientToServer, JsonrpcMessageFromServerToClient, JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* WebSocket close code for session revocation.
|
|
13
|
+
*
|
|
14
|
+
* Also the close for an upgrade refused at admission because its credential
|
|
15
|
+
* was revoked while the upgrade was in flight (`register_action_ws`) — the
|
|
16
|
+
* same outcome, reached a moment earlier.
|
|
17
|
+
*/
|
|
12
18
|
export declare const WS_CLOSE_SESSION_REVOKED = 4001;
|
|
19
|
+
/**
|
|
20
|
+
* The close reason sent with `WS_CLOSE_SESSION_REVOKED` — one definition, so a
|
|
21
|
+
* socket revoked mid-life and an upgrade refused at admission are
|
|
22
|
+
* indistinguishable on the wire. The Rust spine sends the same text.
|
|
23
|
+
*/
|
|
24
|
+
export declare const WS_CLOSE_SESSION_REVOKED_REASON = "Session revoked";
|
|
13
25
|
/** WebSocket close code — client timed out waiting for a response. */
|
|
14
26
|
export declare const WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT = 4002;
|
|
15
27
|
/** WebSocket close code — server timed out with no incoming activity. */
|
|
16
28
|
export declare const WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT = 4003;
|
|
29
|
+
/**
|
|
30
|
+
* WebSocket close code — the server closed this socket to admit a newer
|
|
31
|
+
* connection on the same account, past the per-account connection cap
|
|
32
|
+
* (`BackendWebsocketTransport`'s `max_connections_per_account`, evict-oldest).
|
|
33
|
+
*
|
|
34
|
+
* Distinct from `WS_CLOSE_SESSION_REVOKED`: the credential is still good, so
|
|
35
|
+
* a client must not enter its `revoked` state — but it must not auto-reconnect
|
|
36
|
+
* either, since a reconnect would supersede a newer socket in turn and two
|
|
37
|
+
* tabs past the cap would close each other in a loop. `FrontendWebsocketClient`
|
|
38
|
+
* treats it as closed until the user acts (`superseded`). The twin of the Rust
|
|
39
|
+
* spine's `WS_CLOSE_CONNECTION_LIMIT`.
|
|
40
|
+
*/
|
|
41
|
+
export declare const WS_CLOSE_CONNECTION_LIMIT = 4004;
|
|
17
42
|
/**
|
|
18
43
|
* WebSocket close code — an inbound message exceeded the receiver's size cap
|
|
19
44
|
* (RFC 6455 §7.4.1 "Message Too Big").
|
|
20
45
|
*/
|
|
21
46
|
export declare const WS_CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
47
|
+
/**
|
|
48
|
+
* WebSocket close code — the server closed a socket that sent too much while
|
|
49
|
+
* it was still opening: past `MAX_PRE_ADMISSION_FRAMES` frames or the
|
|
50
|
+
* `PRE_ADMISSION_QUEUE_BYTES_FACTOR` byte budget, before its admission and
|
|
51
|
+
* `on_socket_open` had completed (RFC 6455 §7.4.1 "Policy Violation"). Not a
|
|
52
|
+
* revocation: the client reconnects under its ordinary backoff.
|
|
53
|
+
*/
|
|
54
|
+
export declare const WS_CLOSE_POLICY_VIOLATION = 1008;
|
|
55
|
+
/**
|
|
56
|
+
* WebSocket close code — the server hit an unexpected condition (RFC 6455
|
|
57
|
+
* §7.4.1 "Internal Error").
|
|
58
|
+
*
|
|
59
|
+
* Sent when an upgraded socket can't be admitted because its credential
|
|
60
|
+
* re-check itself failed (a database error), when an `on_socket_open` hook
|
|
61
|
+
* throws, and when a frame arrives on a socket whose connection was removed
|
|
62
|
+
* from the transport without a close (a bare `remove_connection`). The
|
|
63
|
+
* handshake has already answered `101`, so an HTTP `500` is no longer
|
|
64
|
+
* available. Not a revocation: the client's credential may be fine, so it
|
|
65
|
+
* reconnects under its ordinary backoff rather than entering its `revoked`
|
|
66
|
+
* state. The twin of the Rust spine's `WS_CLOSE_INTERNAL_ERROR`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const WS_CLOSE_INTERNAL_ERROR = 1011;
|
|
22
69
|
/**
|
|
23
70
|
* Default cap on one WebSocket message, in UTF-8 bytes — equal to the default
|
|
24
71
|
* HTTP body cap (`DEFAULT_MAX_BODY_SIZE`), so one JSON-RPC request is bounded
|
|
@@ -38,6 +85,13 @@ export declare const DEFAULT_WS_MAX_MESSAGE_BYTES: number;
|
|
|
38
85
|
* @returns the UTF-8 byte length when it exceeds `max_bytes`, else `null`
|
|
39
86
|
*/
|
|
40
87
|
export declare const utf8_length_over: (text: string, max_bytes: number) => number | null;
|
|
88
|
+
/**
|
|
89
|
+
* The UTF-8 byte length of `text`.
|
|
90
|
+
*
|
|
91
|
+
* @param text - the string to measure
|
|
92
|
+
* @returns its length in UTF-8 bytes
|
|
93
|
+
*/
|
|
94
|
+
export declare const utf8_length: (text: string) => number;
|
|
41
95
|
export declare const TransportName: z.ZodString;
|
|
42
96
|
export type TransportName = z.infer<typeof TransportName>;
|
|
43
97
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EACX,gCAAgC,EAChC,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B
|
|
1
|
+
{"version":3,"file":"transports.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EACX,gCAAgC,EAChC,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AACjE,sEAAsE;AACtE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAC9C;;;GAGG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAC9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,QAAc,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,EAAE,WAAW,MAAM,KAAG,MAAM,GAAG,IAI3E,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,MAA8C,CAAC;AAK1F,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACzB,cAAc,EAAE,aAAa,CAAC;IAE9B,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/F,IAAI,CACH,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IACxC,IAAI,CACH,OAAO,EAAE,gCAAgC,EACzC,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC,CAAC;IACpD,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,qBAAa,UAAU;;IAItB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAQ;IAE/B;;;;;OAKG;IACH,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAQ9C;;;;;OAKG;IACH,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,IAAI;IAM1D;;;;;;OAMG;IACH,aAAa,CAAC,cAAc,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;IAO/D,QAAQ,IAAI,OAAO,GAAG,IAAI;IAM1B,qBAAqB,IAAI,SAAS,GAAG,IAAI;IAIzC,0BAA0B,IAAI,aAAa,GAAG,IAAI;IAIlD,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;CAwCtE"}
|
|
@@ -7,17 +7,64 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* WebSocket close code for session revocation.
|
|
12
|
+
*
|
|
13
|
+
* Also the close for an upgrade refused at admission because its credential
|
|
14
|
+
* was revoked while the upgrade was in flight (`register_action_ws`) — the
|
|
15
|
+
* same outcome, reached a moment earlier.
|
|
16
|
+
*/
|
|
11
17
|
export const WS_CLOSE_SESSION_REVOKED = 4001;
|
|
18
|
+
/**
|
|
19
|
+
* The close reason sent with `WS_CLOSE_SESSION_REVOKED` — one definition, so a
|
|
20
|
+
* socket revoked mid-life and an upgrade refused at admission are
|
|
21
|
+
* indistinguishable on the wire. The Rust spine sends the same text.
|
|
22
|
+
*/
|
|
23
|
+
export const WS_CLOSE_SESSION_REVOKED_REASON = 'Session revoked';
|
|
12
24
|
/** WebSocket close code — client timed out waiting for a response. */
|
|
13
25
|
export const WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT = 4002;
|
|
14
26
|
/** WebSocket close code — server timed out with no incoming activity. */
|
|
15
27
|
export const WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT = 4003;
|
|
28
|
+
/**
|
|
29
|
+
* WebSocket close code — the server closed this socket to admit a newer
|
|
30
|
+
* connection on the same account, past the per-account connection cap
|
|
31
|
+
* (`BackendWebsocketTransport`'s `max_connections_per_account`, evict-oldest).
|
|
32
|
+
*
|
|
33
|
+
* Distinct from `WS_CLOSE_SESSION_REVOKED`: the credential is still good, so
|
|
34
|
+
* a client must not enter its `revoked` state — but it must not auto-reconnect
|
|
35
|
+
* either, since a reconnect would supersede a newer socket in turn and two
|
|
36
|
+
* tabs past the cap would close each other in a loop. `FrontendWebsocketClient`
|
|
37
|
+
* treats it as closed until the user acts (`superseded`). The twin of the Rust
|
|
38
|
+
* spine's `WS_CLOSE_CONNECTION_LIMIT`.
|
|
39
|
+
*/
|
|
40
|
+
export const WS_CLOSE_CONNECTION_LIMIT = 4004;
|
|
16
41
|
/**
|
|
17
42
|
* WebSocket close code — an inbound message exceeded the receiver's size cap
|
|
18
43
|
* (RFC 6455 §7.4.1 "Message Too Big").
|
|
19
44
|
*/
|
|
20
45
|
export const WS_CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
46
|
+
/**
|
|
47
|
+
* WebSocket close code — the server closed a socket that sent too much while
|
|
48
|
+
* it was still opening: past `MAX_PRE_ADMISSION_FRAMES` frames or the
|
|
49
|
+
* `PRE_ADMISSION_QUEUE_BYTES_FACTOR` byte budget, before its admission and
|
|
50
|
+
* `on_socket_open` had completed (RFC 6455 §7.4.1 "Policy Violation"). Not a
|
|
51
|
+
* revocation: the client reconnects under its ordinary backoff.
|
|
52
|
+
*/
|
|
53
|
+
export const WS_CLOSE_POLICY_VIOLATION = 1008;
|
|
54
|
+
/**
|
|
55
|
+
* WebSocket close code — the server hit an unexpected condition (RFC 6455
|
|
56
|
+
* §7.4.1 "Internal Error").
|
|
57
|
+
*
|
|
58
|
+
* Sent when an upgraded socket can't be admitted because its credential
|
|
59
|
+
* re-check itself failed (a database error), when an `on_socket_open` hook
|
|
60
|
+
* throws, and when a frame arrives on a socket whose connection was removed
|
|
61
|
+
* from the transport without a close (a bare `remove_connection`). The
|
|
62
|
+
* handshake has already answered `101`, so an HTTP `500` is no longer
|
|
63
|
+
* available. Not a revocation: the client's credential may be fine, so it
|
|
64
|
+
* reconnects under its ordinary backoff rather than entering its `revoked`
|
|
65
|
+
* state. The twin of the Rust spine's `WS_CLOSE_INTERNAL_ERROR`.
|
|
66
|
+
*/
|
|
67
|
+
export const WS_CLOSE_INTERNAL_ERROR = 1011;
|
|
21
68
|
/**
|
|
22
69
|
* Default cap on one WebSocket message, in UTF-8 bytes — equal to the default
|
|
23
70
|
* HTTP body cap (`DEFAULT_MAX_BODY_SIZE`), so one JSON-RPC request is bounded
|
|
@@ -43,6 +90,13 @@ export const utf8_length_over = (text, max_bytes) => {
|
|
|
43
90
|
const size = text_encoder.encode(text).byteLength;
|
|
44
91
|
return size > max_bytes ? size : null;
|
|
45
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* The UTF-8 byte length of `text`.
|
|
95
|
+
*
|
|
96
|
+
* @param text - the string to measure
|
|
97
|
+
* @returns its length in UTF-8 bytes
|
|
98
|
+
*/
|
|
99
|
+
export const utf8_length = (text) => text_encoder.encode(text).byteLength;
|
|
46
100
|
// TODO figure out the symmetry of frontend and backend transports (none/partial/full?) --
|
|
47
101
|
// we may also need orthogonal abstractions to clarify the transport role
|
|
48
102
|
export const TransportName = z.string(); // not branded for convenience, will just error at runtime, the schema is just for docs atm
|
|
@@ -1,101 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WebSocket auth guard —
|
|
2
|
+
* WebSocket auth guard — the audit-event listener that closes live
|
|
3
|
+
* connections when a row reports a revocation.
|
|
3
4
|
*
|
|
4
5
|
* **Why this exists.** `register_action_ws` captures `account_id` and
|
|
5
6
|
* `credential_type` at upgrade time and reuses them for every message.
|
|
6
7
|
* `perform_action`'s per-message authorization phase reloads role_grants
|
|
7
|
-
* from the DB, but session and token VALIDITY are not re-queried
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* revocation actually takes effect on existing connections. Without it,
|
|
13
|
-
* `session_revoke` / `token_revoke` are no-ops for open WS connections.
|
|
8
|
+
* from the DB, but session and token VALIDITY are not re-queried per message
|
|
9
|
+
* — they are re-read once, at admission (`revalidate_resolved_auth`) — a
|
|
10
|
+
* trade-off that keeps chatty WS connections fast. The cost: nothing in the
|
|
11
|
+
* dispatch path notices when a session is revoked or a token is rotated, so a
|
|
12
|
+
* revocation reaches an open connection only when something closes it.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Two things do. The revocation handlers close directly, after their
|
|
15
|
+
* transaction commits (`queue_connection_close` in
|
|
16
|
+
* `actions/connection_closer.ts`) — that close does not depend on the audit
|
|
17
|
+
* write. This guard repeats it when the audit row is announced, which covers
|
|
18
|
+
* a revocation emitted by code that holds no closer. Closing is idempotent, so
|
|
19
|
+
* the overlap is a repeat, not a conflict. A success row is announced only
|
|
20
|
+
* after its request's transaction commits (`auth/audit_emitter.ts`), so the
|
|
21
|
+
* guard's close is post-commit too.
|
|
22
|
+
*
|
|
23
|
+
* ## Which rows close
|
|
24
|
+
*
|
|
25
|
+
* The guard dispatches on the `RevocationScope` each audit event declares in
|
|
26
|
+
* `audit_event_revocation_scopes` (`auth/audit_log_schema.ts`), the table the
|
|
27
|
+
* SSE guard (`realtime/sse_auth_guard.ts`) reads too — one declaration, so the
|
|
28
|
+
* two cannot drift into closing a revoked token's socket while leaving its
|
|
29
|
+
* stream open.
|
|
30
|
+
*
|
|
31
|
+
* `role` is the one scope this guard deliberately does not act on:
|
|
32
|
+
* `perform_action` re-authorizes every message, so a socket whose account just
|
|
33
|
+
* lost the gating role is refused at its next message, and closing it would
|
|
34
|
+
* only change when the caller learns. A stream has no later check, so the SSE
|
|
35
|
+
* guard closes there.
|
|
18
36
|
*
|
|
19
37
|
* For standard WS endpoints mounted via `AppServerOptions.ws_endpoints`,
|
|
20
|
-
* `create_app_server`
|
|
38
|
+
* `create_app_server` registers the guard automatically per
|
|
21
39
|
* `WsEndpointSpec.auth_guard`. For custom wiring, register the handler
|
|
22
40
|
* inside the consumer's `audit_factory` body (or via
|
|
23
41
|
* `audit.add_listener(...)` post-assembly).
|
|
24
42
|
*
|
|
43
|
+
* Twin of the Rust spine's `register_socket_revocation_listeners`.
|
|
44
|
+
*
|
|
25
45
|
* @module
|
|
26
46
|
*/
|
|
27
47
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
28
|
-
import type
|
|
29
|
-
import type {
|
|
48
|
+
import { type AuditLogEvent } from '../auth/audit_log_schema.js';
|
|
49
|
+
import type { ConnectionCloser } from './connection_closer.js';
|
|
30
50
|
/**
|
|
31
51
|
* Audit-event callback shape — the function `CreateAuditEmitterOptions.on_audit_event`
|
|
32
|
-
* accepts and that
|
|
52
|
+
* accepts and that `create_ws_auth_guard` returns.
|
|
33
53
|
*
|
|
34
54
|
* Exported so consumers composing multiple handlers (typically
|
|
35
|
-
* `create_ws_auth_guard` + `
|
|
36
|
-
*
|
|
37
|
-
*
|
|
55
|
+
* `create_ws_auth_guard` + their own pre-existing `on_audit_event`) can
|
|
56
|
+
* annotate their composed callback without reaching for
|
|
57
|
+
* `Parameters<typeof create_ws_auth_guard>[0]`.
|
|
38
58
|
*/
|
|
39
59
|
export type AuditEventHandler = (event: AuditLogEvent) => void;
|
|
40
60
|
/**
|
|
41
|
-
*
|
|
61
|
+
* Create an audit event handler that closes connections on a successful
|
|
62
|
+
* revocation row, dispatching on the event's `RevocationScope`:
|
|
42
63
|
*
|
|
43
|
-
* - `
|
|
44
|
-
* - `
|
|
45
|
-
* - `
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* (out of scope). Consumers that need it compose their own callback.
|
|
52
|
-
*/
|
|
53
|
-
export declare const ws_disconnect_event_types: ReadonlySet<string>;
|
|
54
|
-
/**
|
|
55
|
-
* Create an audit event handler that closes WebSocket connections on auth changes.
|
|
64
|
+
* - `session` → `close_sockets_for_session(metadata.session_id)`
|
|
65
|
+
* - `token` → `close_sockets_for_token(metadata.token_id)`
|
|
66
|
+
* - `account` → `close_sockets_for_account(target_account_id ?? account_id)`
|
|
67
|
+
* — `logout`, `password_change`, `session_revoke_all`, `token_revoke_all`,
|
|
68
|
+
* `account_delete`, `account_purge`
|
|
69
|
+
* - `role` → nothing, deliberately (module doc)
|
|
70
|
+
* - `none` → nothing — every other event, and any event type fuz_app does not
|
|
71
|
+
* define
|
|
56
72
|
*
|
|
57
73
|
* Ignores `outcome === 'failure'` events — they carry attacker-controlled
|
|
58
74
|
* identifiers (e.g. a `session_revoke` that the DB rejected still records
|
|
59
75
|
* the submitted session_id), so reacting to them would let any authenticated
|
|
60
76
|
* user close another user's socket by guessing a session hash or token id.
|
|
61
77
|
*
|
|
78
|
+
* @param closer - what to close on — a `BackendWebsocketTransport`, or a
|
|
79
|
+
* `RealtimeCloser` to reach every transport from one listener
|
|
62
80
|
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
63
81
|
* @returns an `on_audit_event` callback suitable for `create_audit_emitter`'s
|
|
64
82
|
* `on_audit_event` slot, or for registering via
|
|
65
|
-
* `audit.add_listener` post-assembly. The returned callback
|
|
66
|
-
* `
|
|
67
|
-
* `close_sockets_for_session` / `_token` / `_account`) on every relevant event.
|
|
68
|
-
*/
|
|
69
|
-
export declare const create_ws_auth_guard: (transport: BackendWebsocketTransport, log: Logger) => AuditEventHandler;
|
|
70
|
-
/**
|
|
71
|
-
* Create an audit event handler that closes WebSocket connections on
|
|
72
|
-
* user-initiated logout.
|
|
73
|
-
*
|
|
74
|
-
* Sibling helper to `create_ws_auth_guard` — kept separate because
|
|
75
|
-
* `ws_disconnect_event_types` deliberately omits `logout` (admin-initiated
|
|
76
|
-
* revocations use `session_revoke`, while `logout` is the user-initiated
|
|
77
|
-
* case). Multiple consumers hand-rolled this same branch before extraction.
|
|
78
|
-
*
|
|
79
|
-
* Compose with `create_ws_auth_guard` to handle both kinds of disconnect:
|
|
80
|
-
*
|
|
81
|
-
* ```ts
|
|
82
|
-
* const ws_guard = create_ws_auth_guard(transport, log);
|
|
83
|
-
* const ws_logout_closer = create_ws_logout_closer(transport, log);
|
|
84
|
-
* const on_audit_event = (event: AuditLogEvent): void => {
|
|
85
|
-
* ws_guard(event);
|
|
86
|
-
* ws_logout_closer(event);
|
|
87
|
-
* };
|
|
88
|
-
* ```
|
|
89
|
-
*
|
|
90
|
-
* Ignores `outcome === 'failure'` events — failed logouts carry
|
|
91
|
-
* unauthenticated identifiers (no session to close anyway), and reacting
|
|
92
|
-
* to them would let an unauthenticated probe close the targeted account's
|
|
93
|
-
* sockets by submitting a logout for an arbitrary `account_id`.
|
|
94
|
-
*
|
|
95
|
-
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
96
|
-
* @returns an `on_audit_event` callback wireable alongside `create_ws_auth_guard`.
|
|
97
|
-
* The returned callback mutates `transport` via `close_sockets_for_account`
|
|
98
|
-
* on every successful `logout` event with a non-empty `account_id`.
|
|
83
|
+
* `audit.add_listener` post-assembly. The returned callback closes matching
|
|
84
|
+
* connections on `closer` on every relevant event.
|
|
99
85
|
*/
|
|
100
|
-
export declare const
|
|
86
|
+
export declare const create_ws_auth_guard: (closer: ConnectionCloser, log: Logger) => AuditEventHandler;
|
|
101
87
|
//# sourceMappingURL=transports_ws_auth_guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,gBAAgB,EAAE,KAAK,MAAM,KAAG,iBAoD5E,CAAC"}
|