@fuzdev/fuz_app 0.117.1 → 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 +125 -31
- 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 +72 -44
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +60 -16
- 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/bootstrap_account.d.ts +3 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +3 -3
- package/dist/auth/cleanup.d.ts +219 -30
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +260 -45
- package/dist/auth/deps.d.ts +29 -1
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +27 -0
- 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/role_grant_offer_ddl.d.ts +4 -2
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +10 -3
- package/dist/auth/role_grant_offer_queries.d.ts +16 -7
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +33 -15
- package/dist/auth/role_grant_offer_schema.d.ts +9 -0
- package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
- 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/migrate.d.ts +8 -13
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +9 -38
- package/dist/db/status.d.ts +58 -7
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -97
- 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/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +9 -6
- package/dist/runtime/deps.d.ts +3 -2
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/secure_file.d.ts +23 -9
- package/dist/runtime/secure_file.d.ts.map +1 -1
- package/dist/runtime/secure_file.js +29 -10
- package/dist/server/app_backend.d.ts +13 -2
- 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 +3 -52
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +2 -46
- 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/expected_schema.json +1 -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
|
@@ -1,148 +1,124 @@
|
|
|
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
|
*/
|
|
47
|
+
import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
|
|
48
|
+
import { to_revocation_scope } from '../auth/audit_log_schema.js';
|
|
27
49
|
/**
|
|
28
|
-
*
|
|
50
|
+
* Create an audit event handler that closes connections on a successful
|
|
51
|
+
* revocation row, dispatching on the event's `RevocationScope`:
|
|
29
52
|
*
|
|
30
|
-
* - `
|
|
31
|
-
* - `
|
|
32
|
-
* - `
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* (out of scope). Consumers that need it compose their own callback.
|
|
39
|
-
*/
|
|
40
|
-
export const ws_disconnect_event_types = new Set([
|
|
41
|
-
'session_revoke',
|
|
42
|
-
'token_revoke',
|
|
43
|
-
'session_revoke_all',
|
|
44
|
-
'token_revoke_all',
|
|
45
|
-
'password_change'
|
|
46
|
-
]);
|
|
47
|
-
/**
|
|
48
|
-
* Create an audit event handler that closes WebSocket connections on auth changes.
|
|
53
|
+
* - `session` → `close_sockets_for_session(metadata.session_id)`
|
|
54
|
+
* - `token` → `close_sockets_for_token(metadata.token_id)`
|
|
55
|
+
* - `account` → `close_sockets_for_account(target_account_id ?? account_id)`
|
|
56
|
+
* — `logout`, `password_change`, `session_revoke_all`, `token_revoke_all`,
|
|
57
|
+
* `account_delete`, `account_purge`
|
|
58
|
+
* - `role` → nothing, deliberately (module doc)
|
|
59
|
+
* - `none` → nothing — every other event, and any event type fuz_app does not
|
|
60
|
+
* define
|
|
49
61
|
*
|
|
50
62
|
* Ignores `outcome === 'failure'` events — they carry attacker-controlled
|
|
51
63
|
* identifiers (e.g. a `session_revoke` that the DB rejected still records
|
|
52
64
|
* the submitted session_id), so reacting to them would let any authenticated
|
|
53
65
|
* user close another user's socket by guessing a session hash or token id.
|
|
54
66
|
*
|
|
67
|
+
* @param closer - what to close on — a `BackendWebsocketTransport`, or a
|
|
68
|
+
* `RealtimeCloser` to reach every transport from one listener
|
|
55
69
|
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
56
70
|
* @returns an `on_audit_event` callback suitable for `create_audit_emitter`'s
|
|
57
71
|
* `on_audit_event` slot, or for registering via
|
|
58
|
-
* `audit.add_listener` post-assembly. The returned callback
|
|
59
|
-
* `
|
|
60
|
-
* `close_sockets_for_session` / `_token` / `_account`) on every relevant event.
|
|
72
|
+
* `audit.add_listener` post-assembly. The returned callback closes matching
|
|
73
|
+
* connections on `closer` on every relevant event.
|
|
61
74
|
*/
|
|
62
|
-
export const create_ws_auth_guard = (
|
|
75
|
+
export const create_ws_auth_guard = (closer, log) => {
|
|
63
76
|
return (event) => {
|
|
64
|
-
if (!ws_disconnect_event_types.has(event.event_type))
|
|
65
|
-
return;
|
|
66
77
|
// Failed mutations carry attacker-controlled metadata — never act on them.
|
|
67
78
|
if (event.outcome === 'failure')
|
|
68
79
|
return;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
const scope = to_revocation_scope(event.event_type);
|
|
81
|
+
switch (scope) {
|
|
82
|
+
case 'none':
|
|
83
|
+
return;
|
|
84
|
+
// A decision, not a fallthrough: per-message dispatch re-authorizes,
|
|
85
|
+
// so the next message is refused. The SSE guard closes on this scope.
|
|
86
|
+
case 'role':
|
|
87
|
+
return;
|
|
88
|
+
case 'session': {
|
|
89
|
+
const session_id = event.metadata?.session_id;
|
|
90
|
+
if (typeof session_id !== 'string' || session_id.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
const closed = closer.close_sockets_for_session(session_id);
|
|
93
|
+
if (closed > 0) {
|
|
94
|
+
log.info(`WS auth guard: closed ${closed} socket(s) for session ${session_id} (${event.event_type})`);
|
|
95
|
+
}
|
|
72
96
|
return;
|
|
73
|
-
const closed = transport.close_sockets_for_session(session_id);
|
|
74
|
-
if (closed > 0) {
|
|
75
|
-
log.info(`WS auth guard: closed ${closed} socket(s) for session ${session_id} (session_revoke)`);
|
|
76
97
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
case 'token': {
|
|
99
|
+
const token_id = event.metadata?.token_id;
|
|
100
|
+
if (typeof token_id !== 'string' || token_id.length === 0)
|
|
101
|
+
return;
|
|
102
|
+
const closed = closer.close_sockets_for_token(token_id);
|
|
103
|
+
if (closed > 0) {
|
|
104
|
+
log.info(`WS auth guard: closed ${closed} socket(s) for token ${token_id} (${event.event_type})`);
|
|
105
|
+
}
|
|
82
106
|
return;
|
|
83
|
-
const closed = transport.close_sockets_for_token(token_id);
|
|
84
|
-
if (closed > 0) {
|
|
85
|
-
log.info(`WS auth guard: closed ${closed} socket(s) for token ${token_id} (token_revoke)`);
|
|
86
107
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
};
|
|
104
|
-
/**
|
|
105
|
-
* Create an audit event handler that closes WebSocket connections on
|
|
106
|
-
* user-initiated logout.
|
|
107
|
-
*
|
|
108
|
-
* Sibling helper to `create_ws_auth_guard` — kept separate because
|
|
109
|
-
* `ws_disconnect_event_types` deliberately omits `logout` (admin-initiated
|
|
110
|
-
* revocations use `session_revoke`, while `logout` is the user-initiated
|
|
111
|
-
* case). Multiple consumers hand-rolled this same branch before extraction.
|
|
112
|
-
*
|
|
113
|
-
* Compose with `create_ws_auth_guard` to handle both kinds of disconnect:
|
|
114
|
-
*
|
|
115
|
-
* ```ts
|
|
116
|
-
* const ws_guard = create_ws_auth_guard(transport, log);
|
|
117
|
-
* const ws_logout_closer = create_ws_logout_closer(transport, log);
|
|
118
|
-
* const on_audit_event = (event: AuditLogEvent): void => {
|
|
119
|
-
* ws_guard(event);
|
|
120
|
-
* ws_logout_closer(event);
|
|
121
|
-
* };
|
|
122
|
-
* ```
|
|
123
|
-
*
|
|
124
|
-
* Ignores `outcome === 'failure'` events — failed logouts carry
|
|
125
|
-
* unauthenticated identifiers (no session to close anyway), and reacting
|
|
126
|
-
* to them would let an unauthenticated probe close the targeted account's
|
|
127
|
-
* sockets by submitting a logout for an arbitrary `account_id`.
|
|
128
|
-
*
|
|
129
|
-
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
130
|
-
* @returns an `on_audit_event` callback wireable alongside `create_ws_auth_guard`.
|
|
131
|
-
* The returned callback mutates `transport` via `close_sockets_for_account`
|
|
132
|
-
* on every successful `logout` event with a non-empty `account_id`.
|
|
133
|
-
*/
|
|
134
|
-
export const create_ws_logout_closer = (transport, log) => {
|
|
135
|
-
return (event) => {
|
|
136
|
-
if (event.event_type !== 'logout')
|
|
137
|
-
return;
|
|
138
|
-
if (event.outcome === 'failure')
|
|
139
|
-
return;
|
|
140
|
-
const account_id = event.account_id;
|
|
141
|
-
if (!account_id)
|
|
142
|
-
return;
|
|
143
|
-
const closed = transport.close_sockets_for_account(account_id);
|
|
144
|
-
if (closed > 0) {
|
|
145
|
-
log.info(`WS logout closer: closed ${closed} socket(s) for account ${account_id} (logout)`);
|
|
108
|
+
case 'account': {
|
|
109
|
+
// Admin actions set `target_account_id`; self-service actions only
|
|
110
|
+
// set `account_id`.
|
|
111
|
+
const target = event.target_account_id ?? event.account_id;
|
|
112
|
+
if (!target)
|
|
113
|
+
return;
|
|
114
|
+
const closed = closer.close_sockets_for_account(target);
|
|
115
|
+
if (closed > 0) {
|
|
116
|
+
log.info(`WS auth guard: closed ${closed} socket(s) for account ${target} (${event.event_type})`);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
throw new UnreachableError(scope);
|
|
146
122
|
}
|
|
147
123
|
};
|
|
148
124
|
};
|
|
@@ -1,10 +1,53 @@
|
|
|
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 type { WSContext } from 'hono/ws';
|
|
50
|
+
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
8
51
|
import { type Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
9
52
|
import type { JsonrpcMessageFromServerToClient, JsonrpcNotification, JsonrpcRequest, JsonrpcRequestParams, JsonrpcResponse, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
|
|
10
53
|
import { type Transport, type TransportSendOptions } from './transports.js';
|
|
@@ -39,11 +82,103 @@ export interface FilterableBroadcastTransport extends Transport {
|
|
|
39
82
|
}
|
|
40
83
|
/** Type guard for `FilterableBroadcastTransport`. */
|
|
41
84
|
export declare const is_filterable_broadcast_transport: (transport: Transport) => transport is FilterableBroadcastTransport;
|
|
85
|
+
/**
|
|
86
|
+
* Default per-account WebSocket connection cap, applied by
|
|
87
|
+
* `BackendWebsocketTransport` unless overridden via
|
|
88
|
+
* `BackendWebsocketTransportOptions.max_connections_per_account`.
|
|
89
|
+
*
|
|
90
|
+
* Matches the account-wide ceiling the audit SSE cap implies (the default
|
|
91
|
+
* session cap, `DEFAULT_MAX_SESSIONS`, times `AUDIT_LOG_SSE_MAX_PER_SCOPE`
|
|
92
|
+
* streams per session) — generous enough that a user's tabs and tools never
|
|
93
|
+
* meet it; only an abusive or leaking client does. The twin of the Rust
|
|
94
|
+
* spine's `DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT`.
|
|
95
|
+
*/
|
|
96
|
+
export declare const DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT = 50;
|
|
97
|
+
/** Options for `BackendWebsocketTransport`. */
|
|
98
|
+
export interface BackendWebsocketTransportOptions {
|
|
99
|
+
/**
|
|
100
|
+
* Max concurrent connections per `account_id`, across every credential
|
|
101
|
+
* type (session, API token, daemon token — none exempt). The policy is
|
|
102
|
+
* **evict-oldest**: a connection past the cap is always admitted, and the
|
|
103
|
+
* account's oldest connections are closed with `WS_CLOSE_CONNECTION_LIMIT`
|
|
104
|
+
* to make room. Refusing the new one instead would let half-open sockets
|
|
105
|
+
* lock a user out with their own dead connections, and would let a stolen
|
|
106
|
+
* credential fill the slots ahead of the real user — revocation is the fix
|
|
107
|
+
* for a stolen credential, and evict-oldest never stands in its way.
|
|
108
|
+
*
|
|
109
|
+
* The cap is per transport: endpoints sharing one transport share one
|
|
110
|
+
* count per account. `null` disables it.
|
|
111
|
+
*
|
|
112
|
+
* @default DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT
|
|
113
|
+
*/
|
|
114
|
+
max_connections_per_account?: number | null;
|
|
115
|
+
/**
|
|
116
|
+
* Logger for cap evictions and failed sends or closes. A cap eviction logs
|
|
117
|
+
* at info — the account, how many connections were closed, and the cap —
|
|
118
|
+
* so an operator can see a client that keeps meeting it. Unset falls back
|
|
119
|
+
* to a `[ws]` logger; `null` silences it. `register_action_ws` passes its
|
|
120
|
+
* own logger to the transport it creates.
|
|
121
|
+
*/
|
|
122
|
+
log?: Logger | null;
|
|
123
|
+
}
|
|
42
124
|
export declare class BackendWebsocketTransport implements FilterableBroadcastTransport {
|
|
43
125
|
#private;
|
|
44
126
|
readonly transport_name: "backend_websocket_rpc";
|
|
45
127
|
/**
|
|
46
|
-
*
|
|
128
|
+
* @throws Error when `max_connections_per_account` is neither `null` nor a
|
|
129
|
+
* positive integer
|
|
130
|
+
*/
|
|
131
|
+
constructor(options?: BackendWebsocketTransportOptions);
|
|
132
|
+
/**
|
|
133
|
+
* Register a connection **un-admitted** — phase one of the two-phase
|
|
134
|
+
* registration an upgrade runs (module doc).
|
|
135
|
+
*
|
|
136
|
+
* The entry is closeable at once: a `close_sockets_for_*` matching
|
|
137
|
+
* `token_hash` / `account_id` / `api_token_id` removes it, aborts
|
|
138
|
+
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED`; the
|
|
139
|
+
* later `admit` then refuses. Until admitted it evicts nothing, is not
|
|
140
|
+
* counted toward the per-account cap, and receives no message.
|
|
141
|
+
*
|
|
142
|
+
* The caller owns the entry until it is admitted: every refusal path calls
|
|
143
|
+
* `remove_connection` with the returned id.
|
|
144
|
+
*
|
|
145
|
+
* @param ws - the socket to close on revocation
|
|
146
|
+
* @param token_hash - blake3 session token hash, or `null` for non-session credentials
|
|
147
|
+
* @param account_id - the authenticated account
|
|
148
|
+
* @param api_token_id - `api_token.id` for bearer connections, else `null`
|
|
149
|
+
* @param abort_controller - the socket-scoped controller to abort when the
|
|
150
|
+
* transport closes this connection (revocation or cap eviction)
|
|
151
|
+
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
152
|
+
* @mutates this - inserts an un-admitted entry into `#connections`
|
|
153
|
+
*/
|
|
154
|
+
register_pending(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null): Uuid;
|
|
155
|
+
/**
|
|
156
|
+
* Admit a pending connection — phase two (module doc).
|
|
157
|
+
*
|
|
158
|
+
* If the entry is gone — a revocation closed it while it was pending, or it
|
|
159
|
+
* was removed — nothing is evicted and the connection is refused. Otherwise
|
|
160
|
+
* the per-account cap is enforced exactly as in `add_connection`, counting
|
|
161
|
+
* only **admitted** connections, and the entry becomes admitted: it now
|
|
162
|
+
* receives messages, is counted, and can itself be superseded. Admitting an
|
|
163
|
+
* already-admitted connection is a no-op that answers `true`.
|
|
164
|
+
*
|
|
165
|
+
* @param connection_id - the id `register_pending` returned
|
|
166
|
+
* @returns `false` when the connection is no longer registered — the caller
|
|
167
|
+
* closes the socket as revoked
|
|
168
|
+
* @mutates this - marks the entry admitted; past the cap, first removes the
|
|
169
|
+
* account's oldest admitted connections and closes their `WSContext`
|
|
170
|
+
*/
|
|
171
|
+
admit(connection_id: Uuid): boolean;
|
|
172
|
+
/**
|
|
173
|
+
* Register a connection, admitted at once — the one-step form of
|
|
174
|
+
* `register_pending` + `admit`.
|
|
175
|
+
*
|
|
176
|
+
* **Not for an upgrade that authenticated a revocable credential**: a
|
|
177
|
+
* revocation landing between that upgrade's credential check and this call
|
|
178
|
+
* closes nothing, and the connection would be admitted on a dead credential
|
|
179
|
+
* (module doc, "Two-phase registration"). This is for a connection with
|
|
180
|
+
* nothing to revoke behind it.
|
|
181
|
+
*
|
|
47
182
|
* Session connections pass a token hash for targeted revocation.
|
|
48
183
|
* Bearer token connections (`api_token`) pass the `api_token.id` so the
|
|
49
184
|
* socket can be closed when that specific token is revoked without
|
|
@@ -51,51 +186,77 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
51
186
|
* pass `null` for both — they're only reachable via
|
|
52
187
|
* `close_sockets_for_account`.
|
|
53
188
|
*
|
|
189
|
+
* Enforces the per-account cap (`max_connections_per_account`): when
|
|
190
|
+
* `account_id` already holds the maximum, its oldest admitted connections are
|
|
191
|
+
* removed and sent a `WS_CLOSE_CONNECTION_LIMIT` close before the new one is
|
|
192
|
+
* inserted. A removed connection gets no further broadcast or peer request,
|
|
193
|
+
* stops counting toward the cap at once, and has its `abort_controller`
|
|
194
|
+
* aborted; its socket is closed when the close handshake completes. The new
|
|
195
|
+
* connection is always admitted.
|
|
196
|
+
*
|
|
54
197
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
55
|
-
* @mutates this - inserts into `#connections
|
|
56
|
-
*
|
|
198
|
+
* @mutates this - inserts an admitted entry into `#connections`; past the
|
|
199
|
+
* cap, first removes the account's oldest admitted connections and closes
|
|
200
|
+
* their `WSContext`
|
|
57
201
|
*/
|
|
58
|
-
add_connection(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null): Uuid;
|
|
202
|
+
add_connection(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null): Uuid;
|
|
59
203
|
/**
|
|
60
|
-
* Remove a
|
|
61
|
-
*
|
|
204
|
+
* Remove a connection and its auth tracking data, pending or admitted, by
|
|
205
|
+
* the id `register_pending` / `add_connection` returned. Idempotent — safe
|
|
206
|
+
* to call after revocation or a cap eviction has already cleaned up, and a
|
|
207
|
+
* no-op for an unknown id.
|
|
208
|
+
*
|
|
209
|
+
* Keyed by id rather than by `WSContext` because a runtime adapter may hand
|
|
210
|
+
* each socket event its own context object (`hono/bun` does), so the one a
|
|
211
|
+
* close event carries can't identify the connection.
|
|
62
212
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
213
|
+
* Removal only: the socket is not closed and the registered
|
|
214
|
+
* `abort_controller` is not aborted — the caller that ends a socket does
|
|
215
|
+
* both.
|
|
216
|
+
*
|
|
217
|
+
* @mutates this - deletes the connection's entry from `#connections` and
|
|
218
|
+
* settles its pending peer requests as `connection_gone`
|
|
65
219
|
*/
|
|
66
|
-
remove_connection(
|
|
220
|
+
remove_connection(connection_id: Uuid): void;
|
|
67
221
|
/**
|
|
68
|
-
* Close all sockets associated with a specific session token hash
|
|
222
|
+
* Close all sockets associated with a specific session token hash,
|
|
223
|
+
* including a registration still pending admission.
|
|
69
224
|
*
|
|
70
225
|
* @returns the number of sockets closed
|
|
71
|
-
* @mutates this - removes matching connections
|
|
72
|
-
* closes their underlying `WSContext` with
|
|
226
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
227
|
+
* controllers, and closes their underlying `WSContext` with
|
|
228
|
+
* `WS_CLOSE_SESSION_REVOKED`
|
|
73
229
|
*/
|
|
74
230
|
close_sockets_for_session(token_hash: string): number;
|
|
75
231
|
/**
|
|
76
|
-
* Close all sockets associated with a specific account
|
|
232
|
+
* Close all sockets associated with a specific account, including a
|
|
233
|
+
* registration still pending admission.
|
|
77
234
|
*
|
|
78
235
|
* @returns the number of sockets closed
|
|
79
|
-
* @mutates this - removes matching connections
|
|
80
|
-
* closes their underlying `WSContext` with
|
|
236
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
237
|
+
* controllers, and closes their underlying `WSContext` with
|
|
238
|
+
* `WS_CLOSE_SESSION_REVOKED`
|
|
81
239
|
*/
|
|
82
240
|
close_sockets_for_account(account_id: Uuid): number;
|
|
83
241
|
/**
|
|
84
|
-
* Close all sockets associated with a specific API token
|
|
242
|
+
* Close all sockets associated with a specific API token, including a
|
|
243
|
+
* registration still pending admission.
|
|
85
244
|
*
|
|
86
|
-
*
|
|
87
|
-
* tear down the account's
|
|
88
|
-
* tokens' sockets.
|
|
245
|
+
* Closes just this token's sockets, so revoking one token — or evicting
|
|
246
|
+
* one past the token cap — doesn't tear down the account's
|
|
247
|
+
* session-authenticated sockets or other tokens' sockets.
|
|
89
248
|
*
|
|
90
249
|
* @returns the number of sockets closed
|
|
91
|
-
* @mutates this - removes matching connections
|
|
92
|
-
* closes their underlying `WSContext` with
|
|
250
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
251
|
+
* controllers, and closes their underlying `WSContext` with
|
|
252
|
+
* `WS_CLOSE_SESSION_REVOKED`
|
|
93
253
|
*/
|
|
94
254
|
close_sockets_for_token(api_token_id: string): number;
|
|
95
255
|
send(message: JsonrpcRequest, options?: TransportSendOptions): Promise<JsonrpcResponseOrError>;
|
|
96
256
|
send(message: JsonrpcNotification, options?: TransportSendOptions): Promise<JsonrpcErrorResponse | null>;
|
|
97
257
|
/**
|
|
98
|
-
* Broadcast to connections whose identity satisfies a predicate.
|
|
258
|
+
* Broadcast to admitted connections whose identity satisfies a predicate. A
|
|
259
|
+
* registration pending admission is skipped before the predicate runs.
|
|
99
260
|
*
|
|
100
261
|
* Used by the broadcast API when a consumer supplies a subscription ACL hook
|
|
101
262
|
* (e.g. zap's `zap_run_created` only reaches the account that owns the run).
|
|
@@ -106,7 +267,7 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
106
267
|
*/
|
|
107
268
|
broadcast_filtered(message: JsonrpcMessageFromServerToClient, predicate: (identity: ConnectionIdentity) => boolean): number;
|
|
108
269
|
/**
|
|
109
|
-
* Send a message to every socket bound to a specific account.
|
|
270
|
+
* Send a message to every admitted socket bound to a specific account.
|
|
110
271
|
*
|
|
111
272
|
* Targeted per-account fan-out for any flow where the delivery target
|
|
112
273
|
* is a single known account. Prefer this over `broadcast_filtered` when
|
|
@@ -129,7 +290,9 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
129
290
|
* connection. Resolves when the client's matching reply arrives (routed in
|
|
130
291
|
* via `resolve_peer_response`), the deadline elapses (`timeout`), the
|
|
131
292
|
* per-connection cap is hit (`too_many_in_flight`), or the socket closes
|
|
132
|
-
* (`connection_gone`).
|
|
293
|
+
* (`connection_gone`). A connection still pending admission answers
|
|
294
|
+
* `connection_gone` like an unknown one. Never throws — every failure is a
|
|
295
|
+
* `PeerRequestError`.
|
|
133
296
|
*
|
|
134
297
|
* Delegates correlation to `#pending` (id allocation, deadline, cap, drain);
|
|
135
298
|
* this method owns only the socket lookup + the send. Server-issued ids are
|
|
@@ -152,15 +315,34 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
152
315
|
* @mutates this - clears the matched entry from `#pending`
|
|
153
316
|
*/
|
|
154
317
|
resolve_peer_response(connection_id: Uuid, response: JsonrpcResponse | JsonrpcErrorResponse): boolean;
|
|
318
|
+
/**
|
|
319
|
+
* Whether a connection is still registered — and admitted: a registration
|
|
320
|
+
* pending admission answers `false`, like every other reader of the
|
|
321
|
+
* transport.
|
|
322
|
+
*
|
|
323
|
+
* `register_action_ws` checks it before dispatching each inbound message, so
|
|
324
|
+
* a socket the server has closed — revoked, superseded by the cap, or
|
|
325
|
+
* refused at admission — dispatches nothing more while its close handshake
|
|
326
|
+
* is still in flight. The twin of the Rust `ConnectionRegistry::is_registered`.
|
|
327
|
+
*/
|
|
328
|
+
is_registered(connection_id: Uuid): boolean;
|
|
155
329
|
is_ready(): boolean;
|
|
156
330
|
/**
|
|
157
|
-
* Number of currently
|
|
331
|
+
* Number of currently **admitted** WebSocket connections.
|
|
158
332
|
*
|
|
159
|
-
* Read-only counter intended for telemetry, logging, and tests.
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* adapter's
|
|
333
|
+
* Read-only counter intended for telemetry, logging, and tests. A
|
|
334
|
+
* registration still pending admission is not a connection yet and is
|
|
335
|
+
* counted by `get_pending_connection_count` instead. Includes connections
|
|
336
|
+
* that have been closed by the peer but not yet removed by the WS adapter's
|
|
337
|
+
* `onClose` callback.
|
|
163
338
|
*/
|
|
164
339
|
get_connection_count(): number;
|
|
340
|
+
/**
|
|
341
|
+
* Number of registrations awaiting admission — between `register_pending`
|
|
342
|
+
* and `admit` (or their refusal). Each lives for one credential re-check, so
|
|
343
|
+
* a count that stays high means upgrades are stalling there. For telemetry
|
|
344
|
+
* and tests.
|
|
345
|
+
*/
|
|
346
|
+
get_pending_connection_count(): number;
|
|
165
347
|
}
|
|
166
348
|
//# sourceMappingURL=transports_ws_backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"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"}
|