@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.
Files changed (155) hide show
  1. package/dist/actions/CLAUDE.md +301 -73
  2. package/dist/actions/action_rpc.d.ts.map +1 -1
  3. package/dist/actions/action_rpc.js +16 -1
  4. package/dist/actions/connection_closer.d.ts +136 -51
  5. package/dist/actions/connection_closer.d.ts.map +1 -1
  6. package/dist/actions/connection_closer.js +133 -34
  7. package/dist/actions/register_action_ws.d.ts +176 -24
  8. package/dist/actions/register_action_ws.d.ts.map +1 -1
  9. package/dist/actions/register_action_ws.js +581 -241
  10. package/dist/actions/register_ws_endpoint.d.ts +1 -0
  11. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  12. package/dist/actions/register_ws_endpoint.js +1 -0
  13. package/dist/actions/socket.svelte.d.ts +43 -9
  14. package/dist/actions/socket.svelte.d.ts.map +1 -1
  15. package/dist/actions/socket.svelte.js +59 -8
  16. package/dist/actions/transports.d.ts +55 -1
  17. package/dist/actions/transports.d.ts.map +1 -1
  18. package/dist/actions/transports.js +55 -1
  19. package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
  20. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  21. package/dist/actions/transports_ws_auth_guard.js +87 -111
  22. package/dist/actions/transports_ws_backend.d.ts +212 -30
  23. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  24. package/dist/actions/transports_ws_backend.js +327 -70
  25. package/dist/actions/ws_endpoint_spec.d.ts +26 -13
  26. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  27. package/dist/auth/CLAUDE.md +113 -32
  28. package/dist/auth/account_actions.d.ts +12 -18
  29. package/dist/auth/account_actions.d.ts.map +1 -1
  30. package/dist/auth/account_actions.js +47 -33
  31. package/dist/auth/account_routes.d.ts +5 -11
  32. package/dist/auth/account_routes.d.ts.map +1 -1
  33. package/dist/auth/account_routes.js +40 -36
  34. package/dist/auth/account_schema.d.ts +27 -0
  35. package/dist/auth/account_schema.d.ts.map +1 -1
  36. package/dist/auth/account_schema.js +22 -0
  37. package/dist/auth/admin_actions.d.ts +8 -17
  38. package/dist/auth/admin_actions.d.ts.map +1 -1
  39. package/dist/auth/admin_actions.js +33 -21
  40. package/dist/auth/api_token_queries.d.ts +23 -2
  41. package/dist/auth/api_token_queries.d.ts.map +1 -1
  42. package/dist/auth/api_token_queries.js +39 -3
  43. package/dist/auth/audit_emitter.d.ts +61 -38
  44. package/dist/auth/audit_emitter.d.ts.map +1 -1
  45. package/dist/auth/audit_emitter.js +57 -14
  46. package/dist/auth/audit_log_route_schema.js +7 -7
  47. package/dist/auth/audit_log_routes.d.ts +31 -6
  48. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  49. package/dist/auth/audit_log_routes.js +85 -11
  50. package/dist/auth/audit_log_schema.d.ts +48 -0
  51. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  52. package/dist/auth/audit_log_schema.js +58 -0
  53. package/dist/auth/cleanup.d.ts +203 -28
  54. package/dist/auth/cleanup.d.ts.map +1 -1
  55. package/dist/auth/cleanup.js +222 -26
  56. package/dist/auth/deps.d.ts +28 -0
  57. package/dist/auth/deps.d.ts.map +1 -1
  58. package/dist/auth/resolved_auth.d.ts +105 -0
  59. package/dist/auth/resolved_auth.d.ts.map +1 -0
  60. package/dist/auth/resolved_auth.js +124 -0
  61. package/dist/auth/session_middleware.d.ts +8 -1
  62. package/dist/auth/session_middleware.d.ts.map +1 -1
  63. package/dist/auth/session_middleware.js +9 -3
  64. package/dist/auth/session_queries.d.ts +31 -6
  65. package/dist/auth/session_queries.d.ts.map +1 -1
  66. package/dist/auth/session_queries.js +31 -6
  67. package/dist/auth/standard_rpc_actions.d.ts +9 -9
  68. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  69. package/dist/auth/standard_rpc_actions.js +3 -3
  70. package/dist/db/CLAUDE.md +7 -1
  71. package/dist/db/status.d.ts +58 -1
  72. package/dist/db/status.d.ts.map +1 -1
  73. package/dist/db/status.js +205 -77
  74. package/dist/http/CLAUDE.md +7 -3
  75. package/dist/http/db_routes.d.ts +6 -2
  76. package/dist/http/db_routes.d.ts.map +1 -1
  77. package/dist/http/db_routes.js +2 -2
  78. package/dist/periodic.d.ts +80 -0
  79. package/dist/periodic.d.ts.map +1 -0
  80. package/dist/periodic.js +122 -0
  81. package/dist/realtime/sse_auth_guard.d.ts +79 -49
  82. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  83. package/dist/realtime/sse_auth_guard.js +103 -73
  84. package/dist/realtime/subscriber_registry.d.ts +119 -8
  85. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  86. package/dist/realtime/subscriber_registry.js +184 -25
  87. package/dist/server/app_backend.d.ts +12 -1
  88. package/dist/server/app_backend.d.ts.map +1 -1
  89. package/dist/server/app_backend.js +5 -2
  90. package/dist/server/app_server.d.ts +55 -15
  91. package/dist/server/app_server.d.ts.map +1 -1
  92. package/dist/server/app_server.js +41 -13
  93. package/dist/testing/CLAUDE.md +140 -48
  94. package/dist/testing/app_server.d.ts +2 -2
  95. package/dist/testing/app_server.d.ts.map +1 -1
  96. package/dist/testing/app_server.js +11 -2
  97. package/dist/testing/audit_drift_guard.d.ts +1 -48
  98. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  99. package/dist/testing/audit_drift_guard.js +0 -42
  100. package/dist/testing/connection_closer_helpers.d.ts +9 -22
  101. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  102. package/dist/testing/connection_closer_helpers.js +11 -17
  103. package/dist/testing/cross_backend/capabilities.d.ts +67 -13
  104. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  105. package/dist/testing/cross_backend/capabilities.js +27 -12
  106. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  107. package/dist/testing/cross_backend/default_backend_configs.js +21 -0
  108. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  109. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
  110. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  111. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
  112. package/dist/testing/cross_backend/session_cap.d.ts +13 -0
  113. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  114. package/dist/testing/cross_backend/session_cap.js +55 -19
  115. package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
  116. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  117. package/dist/testing/cross_backend/sse_round_trip.js +93 -26
  118. package/dist/testing/cross_backend/testing_server_bun.js +4 -4
  119. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  120. package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
  121. package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
  122. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
  123. package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
  124. package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
  125. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  126. package/dist/testing/cross_backend/ws_round_trip.js +156 -19
  127. package/dist/testing/sse_round_trip.d.ts +6 -4
  128. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  129. package/dist/testing/stubs.d.ts +1 -1
  130. package/dist/testing/stubs.d.ts.map +1 -1
  131. package/dist/testing/stubs.js +6 -4
  132. package/dist/testing/transports/sse_transport.d.ts +1 -1
  133. package/dist/testing/transports/ws_client.d.ts +7 -0
  134. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  135. package/dist/testing/transports/ws_raw_client.d.ts +51 -0
  136. package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
  137. package/dist/testing/transports/ws_raw_client.js +156 -0
  138. package/dist/testing/transports/ws_transport.d.ts +28 -0
  139. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  140. package/dist/testing/transports/ws_transport.js +45 -1
  141. package/dist/testing/ws_round_trip.d.ts +8 -3
  142. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  143. package/dist/testing/ws_round_trip.js +19 -4
  144. package/dist/ui/AdminAccounts.svelte +4 -3
  145. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  146. package/dist/ui/AdminOverview.svelte +3 -1
  147. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  148. package/dist/ui/CLAUDE.md +22 -6
  149. package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
  150. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  151. package/dist/ui/admin_accounts_state.svelte.js +16 -0
  152. package/dist/ui/auth_state.svelte.d.ts +25 -1
  153. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  154. package/dist/ui/auth_state.svelte.js +28 -1
  155. package/package.json +1 -1
@@ -1,148 +1,124 @@
1
1
  /**
2
- * WebSocket auth guard — bridges audit events to `BackendWebsocketTransport`.
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 — that
8
- * trade-off keeps chatty WS connections fast. The cost: nothing in the
9
- * dispatch path notices when a session is revoked or a token is rotated.
10
- * This guard is the enforcement mechanism — it listens on the audit
11
- * chain and closes affected sockets when revocation events fire, so
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
- * Mirror of `realtime/sse_auth_guard.ts` for the backend WebSocket
16
- * transport. Dispatches audit events to the right `close_sockets_for_*`
17
- * method so consumers do not re-implement the switch themselves.
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` composes the guard automatically per
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
- * Audit event types that trigger WebSocket socket closure.
50
+ * Create an audit event handler that closes connections on a successful
51
+ * revocation row, dispatching on the event's `RevocationScope`:
29
52
  *
30
- * - `session_revoke` — close only the socket tied to the revoked session hash.
31
- * - `token_revoke` — close only the socket(s) authenticated with the revoked `api_token.id`.
32
- * - `session_revoke_all` / `token_revoke_all` / `password_change` — close every socket
33
- * for the affected account (all credentials invalidated).
34
- *
35
- * `role_grant_revoke` is intentionally omitted: the WS transport does not track
36
- * per-connection role requirements, so role-scoped disconnection would
37
- * require either closing all sockets (too aggressive) or new tracking
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 mutates
59
- * `transport` (closing matching sockets via
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 = (transport, log) => {
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
- if (event.event_type === 'session_revoke') {
70
- const session_id = event.metadata?.session_id;
71
- if (typeof session_id !== 'string' || session_id.length === 0)
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
- return;
78
- }
79
- if (event.event_type === 'token_revoke') {
80
- const token_id = event.metadata?.token_id;
81
- if (typeof token_id !== 'string' || token_id.length === 0)
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
- return;
88
- }
89
- // session_revoke_all / token_revoke_all / password_change — all of the
90
- // account's credentials are invalidated; close every socket on the account.
91
- // Admin actions set `target_account_id`; self-service actions only set `account_id`.
92
- const target = event.target_account_id ?? event.account_id;
93
- if (!target)
94
- return;
95
- // `target` is a DB account id (string); the transport's account map is
96
- // keyed by the branded `Uuid` used elsewhere in fuz_app. Same value,
97
- // differing type disciplines across the audit-log and transport layers.
98
- const closed = transport.close_sockets_for_account(target);
99
- if (closed > 0) {
100
- log.info(`WS auth guard: closed ${closed} socket(s) for account ${target} (${event.event_type})`);
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 and revocation support.
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
- * Add a new WebSocket connection with auth info.
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`, `#connection_ids`, and
56
- * `#connection_identities`
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 WebSocket connection and its auth tracking data.
61
- * Idempotent — safe to call after revocation has already cleaned up.
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
- * @mutates this - deletes the connection's entries from `#connections`,
64
- * `#connection_ids`, and `#connection_identities`
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(ws: WSContext): void;
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 from internal maps and
72
- * closes their underlying `WSContext` with `WS_CLOSE_SESSION_REVOKED`
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 from internal maps and
80
- * closes their underlying `WSContext` with `WS_CLOSE_SESSION_REVOKED`
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
- * Used on `token_revoke` audit events so revoking one token doesn't
87
- * tear down the account's session-authenticated sockets or other
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 from internal maps and
92
- * closes their underlying `WSContext` with `WS_CLOSE_SESSION_REVOKED`
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`). Never throws — every failure is a `PeerRequestError`.
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 tracked WebSocket connections.
331
+ * Number of currently **admitted** WebSocket connections.
158
332
  *
159
- * Read-only counter intended for telemetry, logging, and tests.
160
- * Counts every entry in the connection map — including connections
161
- * that have been closed by the peer but not yet removed by the WS
162
- * adapter's `onClose` callback.
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;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,KAAK,EAEX,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,EAEN,KAAK,SAAS,EACd,KAAK,oBAAoB,EACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAEN,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,MAAM,mBAAmB,CAAC;AAI3B;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,sEAAsE;IACtE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,4CAA4C;IAC5C,UAAU,EAAE,IAAI,CAAC;IACjB,sEAAsE;IACtE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,4BAA6B,SAAQ,SAAS;IAC9D,kBAAkB,EAAE,CACnB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,KAChD,MAAM,CAAC;CACZ;AAED,qDAAqD;AACrD,eAAO,MAAM,iCAAiC,GAC7C,WAAW,SAAS,KAClB,SAAS,IAAI,4BAEqE,CAAC;AAEtF,qBAAa,yBAA0B,YAAW,4BAA4B;;IAC7E,QAAQ,CAAC,cAAc,EAAG,uBAAuB,CAAU;IAiB3D;;;;;;;;;;;;OAYG;IACH,cAAc,CACb,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,IAAI,EAChB,YAAY,GAAE,MAAM,GAAG,IAAW,GAChC,IAAI;IAQP;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IA0BtC;;;;;;OAMG;IACH,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAIrD;;;;;;OAMG;IACH,yBAAyB,CAAC,UAAU,EAAE,IAAI,GAAG,MAAM;IAInD;;;;;;;;;;OAUG;IACH,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAuB/C,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAyCvC;;;;;;;;;OASG;IACH,kBAAkB,CACjB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,GAClD,MAAM;IAoBT;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,gCAAgC,GAAG,MAAM;IAIpF;;;;;;;;;;;;;;;;;;OAkBG;IACH,kBAAkB,CACjB,aAAa,EAAE,IAAI,EACnB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,oBAAoB,GAAG,SAAS,EACxC,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,kBAAkB,CAAC;IAkB9B;;;;;;;;;;OAUG;IACH,qBAAqB,CACpB,aAAa,EAAE,IAAI,EACnB,QAAQ,EAAE,eAAe,GAAG,oBAAoB,GAC9C,OAAO;IAIV,QAAQ,IAAI,OAAO;IAInB;;;;;;;OAOG;IACH,oBAAoB,IAAI,MAAM;CAG9B"}
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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"}