@fuzdev/fuz_app 0.118.0 → 0.119.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +301 -73
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +16 -1
- package/dist/actions/connection_closer.d.ts +136 -51
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +133 -34
- package/dist/actions/register_action_ws.d.ts +176 -24
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +581 -241
- package/dist/actions/register_ws_endpoint.d.ts +1 -0
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +1 -0
- package/dist/actions/socket.svelte.d.ts +43 -9
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +59 -8
- package/dist/actions/transports.d.ts +55 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +55 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +87 -111
- package/dist/actions/transports_ws_backend.d.ts +212 -30
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +327 -70
- package/dist/actions/ws_endpoint_spec.d.ts +26 -13
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +113 -32
- package/dist/auth/account_actions.d.ts +12 -18
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +47 -33
- package/dist/auth/account_routes.d.ts +5 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +40 -36
- package/dist/auth/account_schema.d.ts +27 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +22 -0
- package/dist/auth/admin_actions.d.ts +8 -17
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +33 -21
- package/dist/auth/api_token_queries.d.ts +23 -2
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +39 -3
- package/dist/auth/audit_emitter.d.ts +61 -38
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +57 -14
- package/dist/auth/audit_log_route_schema.js +7 -7
- package/dist/auth/audit_log_routes.d.ts +31 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +85 -11
- package/dist/auth/audit_log_schema.d.ts +48 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +58 -0
- package/dist/auth/cleanup.d.ts +203 -28
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +222 -26
- package/dist/auth/deps.d.ts +28 -0
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/resolved_auth.d.ts +105 -0
- package/dist/auth/resolved_auth.d.ts.map +1 -0
- package/dist/auth/resolved_auth.js +124 -0
- package/dist/auth/session_middleware.d.ts +8 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +9 -3
- package/dist/auth/session_queries.d.ts +31 -6
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +31 -6
- package/dist/auth/standard_rpc_actions.d.ts +9 -9
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +3 -3
- package/dist/db/CLAUDE.md +7 -1
- package/dist/db/status.d.ts +58 -1
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -77
- package/dist/http/CLAUDE.md +7 -3
- package/dist/http/db_routes.d.ts +6 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +2 -2
- package/dist/periodic.d.ts +80 -0
- package/dist/periodic.d.ts.map +1 -0
- package/dist/periodic.js +122 -0
- package/dist/realtime/sse_auth_guard.d.ts +79 -49
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +103 -73
- package/dist/realtime/subscriber_registry.d.ts +119 -8
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +184 -25
- package/dist/server/app_backend.d.ts +12 -1
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +5 -2
- package/dist/server/app_server.d.ts +55 -15
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +41 -13
- package/dist/testing/CLAUDE.md +140 -48
- package/dist/testing/app_server.d.ts +2 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -2
- package/dist/testing/audit_drift_guard.d.ts +1 -48
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +0 -42
- package/dist/testing/connection_closer_helpers.d.ts +9 -22
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +11 -17
- package/dist/testing/cross_backend/capabilities.d.ts +67 -13
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +27 -12
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +21 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
- package/dist/testing/cross_backend/session_cap.d.ts +13 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +55 -19
- package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +93 -26
- package/dist/testing/cross_backend/testing_server_bun.js +4 -4
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +156 -19
- package/dist/testing/sse_round_trip.d.ts +6 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/sse_transport.d.ts +1 -1
- package/dist/testing/transports/ws_client.d.ts +7 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_raw_client.d.ts +51 -0
- package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_raw_client.js +156 -0
- package/dist/testing/transports/ws_transport.d.ts +28 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +45 -1
- package/dist/testing/ws_round_trip.d.ts +8 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -4
- package/dist/ui/AdminAccounts.svelte +4 -3
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +3 -1
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +22 -6
- package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -0
- package/dist/ui/auth_state.svelte.d.ts +25 -1
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +28 -1
- package/package.json +1 -1
|
@@ -1,58 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SSE auth guard and convenience factory for audit log SSE.
|
|
2
|
+
* SSE auth guard, connection closer, and convenience factory for audit log SSE.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* An SSE stream is authorized once, at open, and then only emits — nothing
|
|
5
|
+
* re-reads its credential or its role. Two things close it when its access is
|
|
6
|
+
* revoked:
|
|
7
|
+
*
|
|
8
|
+
* - `create_sse_connection_closer` adapts a `SubscriberRegistry` to
|
|
9
|
+
* `ConnectionCloser`, so the revocation handlers close streams directly
|
|
10
|
+
* after their transaction commits (`queue_connection_close`), whatever
|
|
11
|
+
* happens to the audit write.
|
|
12
|
+
* - `create_sse_auth_guard` is the audit-event listener: it repeats those
|
|
13
|
+
* closes when the row is announced, and it is the only closer for a role
|
|
14
|
+
* revocation.
|
|
7
15
|
*
|
|
8
16
|
* `create_audit_log_sse` is a convenience factory that combines the registry,
|
|
9
|
-
* guard, and broadcaster — making the secure path the easy path for
|
|
17
|
+
* guard, closer, and broadcaster — making the secure path the easy path for
|
|
18
|
+
* consumers.
|
|
10
19
|
*
|
|
11
20
|
* @module
|
|
12
21
|
*/
|
|
13
22
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
14
23
|
import { type AuditLogEvent } from '../auth/audit_log_schema.js';
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
24
|
+
import type { ConnectionCloser, RealtimeCloser } from '../actions/connection_closer.js';
|
|
25
|
+
import { SubscriberRegistry } from './subscriber_registry.js';
|
|
26
|
+
import type { SseNotification, EventSpec } from './sse.js';
|
|
17
27
|
/** SSE channel the audit-log stream route publishes on. */
|
|
18
28
|
export declare const AUDIT_LOG_CHANNEL = "audit_log";
|
|
19
29
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* client reconnects if still authorized), so the SSE set is the full union.
|
|
23
|
-
*
|
|
24
|
-
* `role_grant_revoke` requires the revoked role to match the guard's `required_role`
|
|
25
|
-
* (or is skipped entirely when `required_role` is `null` — useful for streams
|
|
26
|
-
* not gated by any specific role_grant). The WS half deliberately omits this
|
|
27
|
-
* event (per-message re-authorization picks role changes up there); a one-way
|
|
28
|
-
* SSE stream has no per-message recheck, so it must close here.
|
|
29
|
-
* `session_revoke_all` / `token_revoke_all` / `password_change` / `logout` close
|
|
30
|
-
* every stream for the target account.
|
|
31
|
-
* `session_revoke` closes only the stream tied to the specific revoked session
|
|
32
|
-
* (matched by the blake3 session hash in `event.metadata.session_id`) — closing
|
|
33
|
-
* all of a user's streams for a single-session revoke would be over-aggressive.
|
|
34
|
-
* The single `token_revoke` the WS half handles is omitted, and what makes that
|
|
35
|
-
* correct is a gate rather than an assumption: the audit route declares
|
|
36
|
-
* `credential_types: ['session']` (`auth/audit_log_route_schema.ts`), so a
|
|
37
|
-
* bearer is refused at the channel and no stream is ever keyed by a single
|
|
38
|
-
* token id. **A consumer that widens that gate must key its subscribers by
|
|
39
|
-
* api-token id and add `token_revoke` here** — otherwise a revoked token keeps
|
|
40
|
-
* receiving audit rows for the life of the connection. The Rust twin enforces
|
|
41
|
-
* the same pairing structurally, via the `RevocationScope` column on
|
|
42
|
-
* `AUDIT_EVENT_SPECS`.
|
|
43
|
-
*/
|
|
44
|
-
export declare const disconnect_event_types: ReadonlySet<string>;
|
|
45
|
-
/**
|
|
46
|
-
* Create an audit event handler that closes SSE streams on auth changes.
|
|
30
|
+
* Adapt a `SubscriberRegistry` to `ConnectionCloser`, so a revocation handler's
|
|
31
|
+
* close reaches its streams.
|
|
47
32
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
33
|
+
* Each method is `SubscriberRegistry.close_by_identity` on the id it is given,
|
|
34
|
+
* so it closes the streams registered under that id as their `scope` or in
|
|
35
|
+
* their `groups` — pending registrations included. The audit stream route
|
|
36
|
+
* registers `scope = session hash` and `groups = [account id]`; a route that
|
|
37
|
+
* admits bearer tokens registers the API token id too, and
|
|
38
|
+
* `close_sockets_for_token` then reaches it.
|
|
53
39
|
*
|
|
54
|
-
*
|
|
55
|
-
|
|
40
|
+
* Twin of the Rust spine's `SocketRevoker` impl on `SseRegistry`.
|
|
41
|
+
*/
|
|
42
|
+
export declare const create_sse_connection_closer: <T>(registry: SubscriberRegistry<T>) => ConnectionCloser;
|
|
43
|
+
/**
|
|
44
|
+
* Create an audit event handler that closes SSE streams on a successful
|
|
45
|
+
* revocation row, dispatching on the event's `RevocationScope`
|
|
46
|
+
* (`audit_event_revocation_scopes`, the table `create_ws_auth_guard` reads
|
|
47
|
+
* too):
|
|
48
|
+
*
|
|
49
|
+
* - `session` → the stream registered under the revoked session's hash
|
|
50
|
+
* (`metadata.session_id`). Closing all of an account's streams for a
|
|
51
|
+
* single-session revoke would be over-aggressive.
|
|
52
|
+
* - `token` → the stream registered under the revoked API token's id
|
|
53
|
+
* (`metadata.token_id`). The audit stream route admits sessions only, so no
|
|
54
|
+
* stream of its own is keyed that way; a consumer route that admits bearers
|
|
55
|
+
* registers the token id and is closed here.
|
|
56
|
+
* - `account` → every stream of `target_account_id ?? account_id`.
|
|
57
|
+
* - `role` → every stream of the target account, when the revoked role
|
|
58
|
+
* (`metadata.role`) is `required_role`. The WebSocket guard does not act on
|
|
59
|
+
* this scope — per-message dispatch re-authorizes there — but a one-way
|
|
60
|
+
* stream has no later check, so it must close here.
|
|
61
|
+
* - `none` → nothing.
|
|
62
|
+
*
|
|
63
|
+
* Over-closing a one-way feed is cheap: the client reconnects if it is still
|
|
64
|
+
* authorized.
|
|
65
|
+
*
|
|
66
|
+
* The registry's subscribers must carry `account_id` as an identity key (in
|
|
67
|
+
* `SubscribeOptions.groups`), and the session hash as `scope` for the
|
|
68
|
+
* session-scoped close.
|
|
56
69
|
*
|
|
57
70
|
* @param registry - the subscriber registry to guard
|
|
58
71
|
* @param required_role - the role that grants access to the SSE endpoint,
|
|
@@ -69,14 +82,17 @@ export declare const create_sse_auth_guard: <T>(registry: SubscriberRegistry<T>,
|
|
|
69
82
|
* `on_audit_event` callback (broadcast + guard).
|
|
70
83
|
*/
|
|
71
84
|
export interface AuditLogSse {
|
|
72
|
-
/**
|
|
73
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The subscriber registry the audit stream route registers each stream on
|
|
87
|
+
* (two-phase: `subscribe_pending` then `admit`) — pass as part of the
|
|
88
|
+
* `stream` option to `create_audit_log_route_specs`. Also exposed for
|
|
89
|
+
* subscriber count monitoring.
|
|
90
|
+
*/
|
|
91
|
+
registry: SubscriberRegistry<SseNotification>;
|
|
74
92
|
/** Logger — pass as part of `stream` option to `create_audit_log_route_specs`. */
|
|
75
93
|
log: Logger;
|
|
76
94
|
/** Combined broadcast + guard callback. Wired by `create_app_server`'s `audit_log_sse` option, or compose inside the consumer's `audit_factory` body. */
|
|
77
95
|
on_audit_event: (event: AuditLogEvent) => void;
|
|
78
|
-
/** The underlying registry — exposed for subscriber count monitoring. */
|
|
79
|
-
registry: SubscriberRegistry<SseNotification>;
|
|
80
96
|
}
|
|
81
97
|
/**
|
|
82
98
|
* SSE event specs for audit log events.
|
|
@@ -97,10 +113,12 @@ export declare const audit_log_event_specs: Array<EventSpec>;
|
|
|
97
113
|
*/
|
|
98
114
|
export declare const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
|
|
99
115
|
/**
|
|
100
|
-
* Create a complete audit log SSE setup with broadcasting
|
|
116
|
+
* Create a complete audit log SSE setup with broadcasting, auth guard, and
|
|
117
|
+
* connection closer.
|
|
101
118
|
*
|
|
102
119
|
* Combines `SubscriberRegistry`, `create_sse_auth_guard`, and the broadcast
|
|
103
|
-
* call into a single object
|
|
120
|
+
* call into a single object, and adds the registry to `connection_closer`.
|
|
121
|
+
* The result satisfies `AuditLogRouteOptions['stream']`
|
|
104
122
|
* and provides the `on_audit_event` listener for the audit emitter.
|
|
105
123
|
*
|
|
106
124
|
* Most consumers pass `audit_log_sse: true` to `create_app_server` and never
|
|
@@ -116,9 +134,12 @@ export declare const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
|
|
|
116
134
|
*
|
|
117
135
|
* @example
|
|
118
136
|
* ```ts
|
|
119
|
-
* const
|
|
137
|
+
* const connection_closer = create_realtime_closer();
|
|
138
|
+
* const audit_sse = create_audit_log_sse({log, connection_closer});
|
|
120
139
|
*
|
|
121
|
-
* //
|
|
140
|
+
* // On CreateAppBackendOptions — the same closer, and the listener inside
|
|
141
|
+
* // the audit_factory body:
|
|
142
|
+
* connection_closer,
|
|
122
143
|
* audit_factory: ({db, log}) => create_audit_emitter({
|
|
123
144
|
* db,
|
|
124
145
|
* log,
|
|
@@ -136,6 +157,15 @@ export declare const create_audit_log_sse: (options: {
|
|
|
136
157
|
/** Role required to access the SSE endpoint. Default `'admin'`. */
|
|
137
158
|
role?: string;
|
|
138
159
|
log: Logger;
|
|
160
|
+
/**
|
|
161
|
+
* The backend's closer — `deps.connection_closer`. The new registry is
|
|
162
|
+
* added to it, so every revocation handler's close reaches these streams.
|
|
163
|
+
* Required so it can't be forgotten: without it a revoked credential's
|
|
164
|
+
* streams close only through the audit listener, which a failed audit write
|
|
165
|
+
* or a cap eviction never reaches. Pass `null` only for a registry no
|
|
166
|
+
* revocation has to reach (tests of the registry itself).
|
|
167
|
+
*/
|
|
168
|
+
connection_closer: RealtimeCloser | null;
|
|
139
169
|
/**
|
|
140
170
|
* Max concurrent SSE subscribers per session scope. On overflow, the oldest
|
|
141
171
|
* matching subscriber is closed. Default `AUDIT_LOG_SSE_MAX_PER_SCOPE`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAIN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE3D,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,4BAA4B,GAAI,CAAC,EAC7C,UAAU,kBAAkB,CAAC,CAAC,CAAC,KAC7B,gBAID,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,GAAG,IAAI,EAC5B,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA4DjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;;OAKG;IACH,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;IAC9C,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,yJAAyJ;IACzJ,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAC/C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,iBAAiB,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
|
|
@@ -1,63 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SSE auth guard and convenience factory for audit log SSE.
|
|
2
|
+
* SSE auth guard, connection closer, and convenience factory for audit log SSE.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* An SSE stream is authorized once, at open, and then only emits — nothing
|
|
5
|
+
* re-reads its credential or its role. Two things close it when its access is
|
|
6
|
+
* revoked:
|
|
7
|
+
*
|
|
8
|
+
* - `create_sse_connection_closer` adapts a `SubscriberRegistry` to
|
|
9
|
+
* `ConnectionCloser`, so the revocation handlers close streams directly
|
|
10
|
+
* after their transaction commits (`queue_connection_close`), whatever
|
|
11
|
+
* happens to the audit write.
|
|
12
|
+
* - `create_sse_auth_guard` is the audit-event listener: it repeats those
|
|
13
|
+
* closes when the row is announced, and it is the only closer for a role
|
|
14
|
+
* revocation.
|
|
7
15
|
*
|
|
8
16
|
* `create_audit_log_sse` is a convenience factory that combines the registry,
|
|
9
|
-
* guard, and broadcaster — making the secure path the easy path for
|
|
17
|
+
* guard, closer, and broadcaster — making the secure path the easy path for
|
|
18
|
+
* consumers.
|
|
10
19
|
*
|
|
11
20
|
* @module
|
|
12
21
|
*/
|
|
13
|
-
import {
|
|
22
|
+
import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
|
|
23
|
+
import { AUDIT_EVENT_TYPES, AuditLogEventJson, to_revocation_scope } from '../auth/audit_log_schema.js';
|
|
14
24
|
import { SubscriberRegistry } from './subscriber_registry.js';
|
|
15
25
|
/** SSE channel the audit-log stream route publishes on. */
|
|
16
26
|
export const AUDIT_LOG_CHANNEL = 'audit_log';
|
|
17
27
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* client reconnects if still authorized), so the SSE set is the full union.
|
|
21
|
-
*
|
|
22
|
-
* `role_grant_revoke` requires the revoked role to match the guard's `required_role`
|
|
23
|
-
* (or is skipped entirely when `required_role` is `null` — useful for streams
|
|
24
|
-
* not gated by any specific role_grant). The WS half deliberately omits this
|
|
25
|
-
* event (per-message re-authorization picks role changes up there); a one-way
|
|
26
|
-
* SSE stream has no per-message recheck, so it must close here.
|
|
27
|
-
* `session_revoke_all` / `token_revoke_all` / `password_change` / `logout` close
|
|
28
|
-
* every stream for the target account.
|
|
29
|
-
* `session_revoke` closes only the stream tied to the specific revoked session
|
|
30
|
-
* (matched by the blake3 session hash in `event.metadata.session_id`) — closing
|
|
31
|
-
* all of a user's streams for a single-session revoke would be over-aggressive.
|
|
32
|
-
* The single `token_revoke` the WS half handles is omitted, and what makes that
|
|
33
|
-
* correct is a gate rather than an assumption: the audit route declares
|
|
34
|
-
* `credential_types: ['session']` (`auth/audit_log_route_schema.ts`), so a
|
|
35
|
-
* bearer is refused at the channel and no stream is ever keyed by a single
|
|
36
|
-
* token id. **A consumer that widens that gate must key its subscribers by
|
|
37
|
-
* api-token id and add `token_revoke` here** — otherwise a revoked token keeps
|
|
38
|
-
* receiving audit rows for the life of the connection. The Rust twin enforces
|
|
39
|
-
* the same pairing structurally, via the `RevocationScope` column on
|
|
40
|
-
* `AUDIT_EVENT_SPECS`.
|
|
41
|
-
*/
|
|
42
|
-
export const disconnect_event_types = new Set([
|
|
43
|
-
'role_grant_revoke', // role revoked — user lost access
|
|
44
|
-
'session_revoke', // single session revoked — close only that stream
|
|
45
|
-
'session_revoke_all', // all sessions invalidated — user should be kicked
|
|
46
|
-
'token_revoke_all', // all API tokens invalidated — close the account's streams
|
|
47
|
-
'password_change', // password changed — all sessions revoked implicitly
|
|
48
|
-
'logout' // explicit logout — close the account's streams
|
|
49
|
-
]);
|
|
50
|
-
/**
|
|
51
|
-
* Create an audit event handler that closes SSE streams on auth changes.
|
|
28
|
+
* Adapt a `SubscriberRegistry` to `ConnectionCloser`, so a revocation handler's
|
|
29
|
+
* close reaches its streams.
|
|
52
30
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
31
|
+
* Each method is `SubscriberRegistry.close_by_identity` on the id it is given,
|
|
32
|
+
* so it closes the streams registered under that id as their `scope` or in
|
|
33
|
+
* their `groups` — pending registrations included. The audit stream route
|
|
34
|
+
* registers `scope = session hash` and `groups = [account id]`; a route that
|
|
35
|
+
* admits bearer tokens registers the API token id too, and
|
|
36
|
+
* `close_sockets_for_token` then reaches it.
|
|
58
37
|
*
|
|
59
|
-
*
|
|
60
|
-
|
|
38
|
+
* Twin of the Rust spine's `SocketRevoker` impl on `SseRegistry`.
|
|
39
|
+
*/
|
|
40
|
+
export const create_sse_connection_closer = (registry) => ({
|
|
41
|
+
close_sockets_for_session: (session_token_hash) => registry.close_by_identity(session_token_hash),
|
|
42
|
+
close_sockets_for_token: (api_token_id) => registry.close_by_identity(api_token_id),
|
|
43
|
+
close_sockets_for_account: (account_id) => registry.close_by_identity(account_id)
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Create an audit event handler that closes SSE streams on a successful
|
|
47
|
+
* revocation row, dispatching on the event's `RevocationScope`
|
|
48
|
+
* (`audit_event_revocation_scopes`, the table `create_ws_auth_guard` reads
|
|
49
|
+
* too):
|
|
50
|
+
*
|
|
51
|
+
* - `session` → the stream registered under the revoked session's hash
|
|
52
|
+
* (`metadata.session_id`). Closing all of an account's streams for a
|
|
53
|
+
* single-session revoke would be over-aggressive.
|
|
54
|
+
* - `token` → the stream registered under the revoked API token's id
|
|
55
|
+
* (`metadata.token_id`). The audit stream route admits sessions only, so no
|
|
56
|
+
* stream of its own is keyed that way; a consumer route that admits bearers
|
|
57
|
+
* registers the token id and is closed here.
|
|
58
|
+
* - `account` → every stream of `target_account_id ?? account_id`.
|
|
59
|
+
* - `role` → every stream of the target account, when the revoked role
|
|
60
|
+
* (`metadata.role`) is `required_role`. The WebSocket guard does not act on
|
|
61
|
+
* this scope — per-message dispatch re-authorizes there — but a one-way
|
|
62
|
+
* stream has no later check, so it must close here.
|
|
63
|
+
* - `none` → nothing.
|
|
64
|
+
*
|
|
65
|
+
* Over-closing a one-way feed is cheap: the client reconnects if it is still
|
|
66
|
+
* authorized.
|
|
67
|
+
*
|
|
68
|
+
* The registry's subscribers must carry `account_id` as an identity key (in
|
|
69
|
+
* `SubscribeOptions.groups`), and the session hash as `scope` for the
|
|
70
|
+
* session-scoped close.
|
|
61
71
|
*
|
|
62
72
|
* @param registry - the subscriber registry to guard
|
|
63
73
|
* @param required_role - the role that grants access to the SSE endpoint,
|
|
@@ -68,8 +78,6 @@ export const disconnect_event_types = new Set([
|
|
|
68
78
|
*/
|
|
69
79
|
export const create_sse_auth_guard = (registry, required_role, log) => {
|
|
70
80
|
return (event) => {
|
|
71
|
-
if (!disconnect_event_types.has(event.event_type))
|
|
72
|
-
return;
|
|
73
81
|
// Only act on successful revocations. Failed attempts carry
|
|
74
82
|
// attacker-controlled identifiers (e.g., session_revoke with outcome=failure
|
|
75
83
|
// carries the submitted session_id even when the DB rejected the cross-account
|
|
@@ -77,29 +85,46 @@ export const create_sse_auth_guard = (registry, required_role, log) => {
|
|
|
77
85
|
// user's SSE stream by guessing or leaking a session hash.
|
|
78
86
|
if (event.outcome === 'failure')
|
|
79
87
|
return;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (event.event_type === 'session_revoke') {
|
|
84
|
-
const session_id = event.metadata?.session_id;
|
|
85
|
-
if (typeof session_id !== 'string' || session_id.length === 0)
|
|
88
|
+
const scope = to_revocation_scope(event.event_type);
|
|
89
|
+
switch (scope) {
|
|
90
|
+
case 'none':
|
|
86
91
|
return;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (event.event_type === 'role_grant_revoke') {
|
|
96
|
-
if (required_role === null)
|
|
92
|
+
case 'session': {
|
|
93
|
+
const session_id = event.metadata?.session_id;
|
|
94
|
+
if (typeof session_id !== 'string' || session_id.length === 0)
|
|
95
|
+
return;
|
|
96
|
+
const closed = registry.close_by_identity(session_id);
|
|
97
|
+
if (closed > 0) {
|
|
98
|
+
log.info(`SSE auth guard: closed ${closed} stream(s) for session ${session_id} (${event.event_type})`);
|
|
99
|
+
}
|
|
97
100
|
return;
|
|
98
|
-
|
|
101
|
+
}
|
|
102
|
+
case 'token': {
|
|
103
|
+
const token_id = event.metadata?.token_id;
|
|
104
|
+
if (typeof token_id !== 'string' || token_id.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
const closed = registry.close_by_identity(token_id);
|
|
107
|
+
if (closed > 0) {
|
|
108
|
+
log.info(`SSE auth guard: closed ${closed} stream(s) for token ${token_id} (${event.event_type})`);
|
|
109
|
+
}
|
|
99
110
|
return;
|
|
111
|
+
}
|
|
112
|
+
case 'role':
|
|
113
|
+
// `null` means the stream isn't gated by a specific role_grant, so
|
|
114
|
+
// a role revocation closes nothing.
|
|
115
|
+
if (required_role === null)
|
|
116
|
+
return;
|
|
117
|
+
if (event.metadata?.role !== required_role)
|
|
118
|
+
return;
|
|
119
|
+
break;
|
|
120
|
+
case 'account':
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
throw new UnreachableError(scope);
|
|
100
124
|
}
|
|
101
|
-
//
|
|
102
|
-
//
|
|
125
|
+
// `role` and `account` both close the affected account's streams —
|
|
126
|
+
// admin actions set target_account_id, self-service actions only set
|
|
127
|
+
// account_id
|
|
103
128
|
const target = event.target_account_id ?? event.account_id;
|
|
104
129
|
if (!target)
|
|
105
130
|
return;
|
|
@@ -133,10 +158,12 @@ export const audit_log_event_specs = AUDIT_EVENT_TYPES.map((event_type) => ({
|
|
|
133
158
|
*/
|
|
134
159
|
export const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
|
|
135
160
|
/**
|
|
136
|
-
* Create a complete audit log SSE setup with broadcasting
|
|
161
|
+
* Create a complete audit log SSE setup with broadcasting, auth guard, and
|
|
162
|
+
* connection closer.
|
|
137
163
|
*
|
|
138
164
|
* Combines `SubscriberRegistry`, `create_sse_auth_guard`, and the broadcast
|
|
139
|
-
* call into a single object
|
|
165
|
+
* call into a single object, and adds the registry to `connection_closer`.
|
|
166
|
+
* The result satisfies `AuditLogRouteOptions['stream']`
|
|
140
167
|
* and provides the `on_audit_event` listener for the audit emitter.
|
|
141
168
|
*
|
|
142
169
|
* Most consumers pass `audit_log_sse: true` to `create_app_server` and never
|
|
@@ -152,9 +179,12 @@ export const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
|
|
|
152
179
|
*
|
|
153
180
|
* @example
|
|
154
181
|
* ```ts
|
|
155
|
-
* const
|
|
182
|
+
* const connection_closer = create_realtime_closer();
|
|
183
|
+
* const audit_sse = create_audit_log_sse({log, connection_closer});
|
|
156
184
|
*
|
|
157
|
-
* //
|
|
185
|
+
* // On CreateAppBackendOptions — the same closer, and the listener inside
|
|
186
|
+
* // the audit_factory body:
|
|
187
|
+
* connection_closer,
|
|
158
188
|
* audit_factory: ({db, log}) => create_audit_emitter({
|
|
159
189
|
* db,
|
|
160
190
|
* log,
|
|
@@ -173,8 +203,8 @@ export const create_audit_log_sse = (options) => {
|
|
|
173
203
|
const max_per_scope = options.max_per_scope === undefined ? AUDIT_LOG_SSE_MAX_PER_SCOPE : options.max_per_scope;
|
|
174
204
|
const registry = new SubscriberRegistry({ max_per_scope });
|
|
175
205
|
const guard = create_sse_auth_guard(registry, role, options.log);
|
|
206
|
+
options.connection_closer?.add(create_sse_connection_closer(registry));
|
|
176
207
|
return {
|
|
177
|
-
subscribe: registry.subscribe.bind(registry),
|
|
178
208
|
log: options.log,
|
|
179
209
|
on_audit_event: (event) => {
|
|
180
210
|
registry.broadcast(AUDIT_LOG_CHANNEL, { method: event.event_type, params: event });
|
|
@@ -17,9 +17,13 @@
|
|
|
17
17
|
* carries a broader identity for coarse close — the broader identity
|
|
18
18
|
* doesn't cap across sessions.
|
|
19
19
|
*
|
|
20
|
+
* Registration can run in two phases (`subscribe_pending` then `admit`) so a
|
|
21
|
+
* revocation reaches a stream that is still opening — see the class doc.
|
|
22
|
+
*
|
|
20
23
|
* @module
|
|
21
24
|
*/
|
|
22
25
|
import type { SseStream } from './sse.js';
|
|
26
|
+
/** One admitted subscriber, as `SubscriberRegistry.broadcast` sees it. */
|
|
23
27
|
export interface Subscriber<T> {
|
|
24
28
|
stream: SseStream<T>;
|
|
25
29
|
/** Channels this subscriber listens to. `null` means all channels. */
|
|
@@ -32,14 +36,14 @@ export interface Subscriber<T> {
|
|
|
32
36
|
/** Options for `SubscriberRegistry`. */
|
|
33
37
|
export interface SubscriberRegistryOptions {
|
|
34
38
|
/**
|
|
35
|
-
* Max subscribers sharing a single `scope`.
|
|
36
|
-
* of subscribers with the same `scope`
|
|
37
|
-
* matching subscriber(s) are closed
|
|
39
|
+
* Max admitted subscribers sharing a single `scope`. When a subscriber is
|
|
40
|
+
* admitted and the count of admitted subscribers with the same `scope` has
|
|
41
|
+
* reached this limit, the oldest matching subscriber(s) are closed first.
|
|
38
42
|
* `null` (default) disables the cap. `groups` identities are never capped.
|
|
39
43
|
*/
|
|
40
44
|
max_per_scope?: number | null;
|
|
41
45
|
}
|
|
42
|
-
/** Options for `SubscriberRegistry.subscribe`. */
|
|
46
|
+
/** Options for `SubscriberRegistry.subscribe` and `subscribe_pending`. */
|
|
43
47
|
export interface SubscribeOptions {
|
|
44
48
|
/** Channels to subscribe to. Empty/absent = all channels. */
|
|
45
49
|
channels?: ReadonlyArray<string>;
|
|
@@ -54,6 +58,23 @@ export interface SubscribeOptions {
|
|
|
54
58
|
*/
|
|
55
59
|
groups?: ReadonlyArray<string>;
|
|
56
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* A registered subscription awaiting admission — what
|
|
63
|
+
* `SubscriberRegistry.subscribe_pending` returns and
|
|
64
|
+
* `SubscriberRegistry.admit` takes.
|
|
65
|
+
*
|
|
66
|
+
* Its entry is already in the registry, so every `close_by_identity` can reach
|
|
67
|
+
* it, but nothing is delivered to it and it does not count toward the
|
|
68
|
+
* per-scope cap until it is admitted.
|
|
69
|
+
*/
|
|
70
|
+
export interface PendingSubscription {
|
|
71
|
+
/**
|
|
72
|
+
* Remove the registration, pending or admitted. Idempotent. Every path that
|
|
73
|
+
* refuses a pending subscription calls it, and an admitted stream's
|
|
74
|
+
* `on_close` listener does.
|
|
75
|
+
*/
|
|
76
|
+
unsubscribe: () => void;
|
|
77
|
+
}
|
|
57
78
|
/**
|
|
58
79
|
* Generic subscriber registry with channel-based filtering and identity-keyed disconnection.
|
|
59
80
|
*
|
|
@@ -62,6 +83,28 @@ export interface SubscribeOptions {
|
|
|
62
83
|
* matching subscribers. `close_by_identity` force-closes all subscribers
|
|
63
84
|
* whose `scope` or `groups` contain the given key — use for auth revocation.
|
|
64
85
|
*
|
|
86
|
+
* ## Two-phase registration
|
|
87
|
+
*
|
|
88
|
+
* A stream is authorized once, at open, so a revocation has to be able to
|
|
89
|
+
* reach it from the moment its credential is read. A route's gates run before
|
|
90
|
+
* the stream exists, and each revocation closes only the streams *registered*
|
|
91
|
+
* under what it revoked — registering after the gates leaves a window in which
|
|
92
|
+
* a revocation closes nothing and the stream then opens on a dead session, or
|
|
93
|
+
* under a role its actor no longer holds.
|
|
94
|
+
*
|
|
95
|
+
* So a route that authenticated a revocable credential registers in two
|
|
96
|
+
* phases: `subscribe_pending` registers the subscription **un-admitted** —
|
|
97
|
+
* closeable by every `close_by_identity`, and otherwise inert: it has no
|
|
98
|
+
* stream, so no broadcast reaches it, the cap neither counts nor evicts for
|
|
99
|
+
* it, and `count` does not see it (`pending_count` does). The route then
|
|
100
|
+
* re-reads the credential and the role, and `admit` attaches the stream and
|
|
101
|
+
* opens the feed — or refuses when a revocation closed the registration in
|
|
102
|
+
* between, evicting nothing. The cap's eviction happens at admission.
|
|
103
|
+
*
|
|
104
|
+
* `subscribe` stays as the one-step form for a stream with nothing to revoke
|
|
105
|
+
* behind it. The twin of the Rust `fuz_realtime` `SseRegistry`
|
|
106
|
+
* (`subscribe_pending` / `admit` / `subscribe`).
|
|
107
|
+
*
|
|
65
108
|
* @example
|
|
66
109
|
* ```ts
|
|
67
110
|
* const registry = new SubscriberRegistry<SseNotification>();
|
|
@@ -91,14 +134,42 @@ export interface SubscribeOptions {
|
|
|
91
134
|
* // fine — close just the stream(s) tied to a specific session
|
|
92
135
|
* registry.close_by_identity(session_hash);
|
|
93
136
|
* ```
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* // two-phase, for a route behind a revocable credential
|
|
141
|
+
* const pending = registry.subscribe_pending({scope: session_hash, groups: [account_id]});
|
|
142
|
+
* if (!(await credential_is_still_live())) {
|
|
143
|
+
* pending.unsubscribe();
|
|
144
|
+
* return unauthorized();
|
|
145
|
+
* }
|
|
146
|
+
* const {response, stream} = create_sse_response(c, log);
|
|
147
|
+
* if (registry.admit(pending, stream)) stream.on_close(pending.unsubscribe);
|
|
148
|
+
* else stream.close(); // closed while pending — the body is the connect comment alone
|
|
149
|
+
* return response;
|
|
150
|
+
* ```
|
|
94
151
|
*/
|
|
95
152
|
export declare class SubscriberRegistry<T> {
|
|
96
153
|
#private;
|
|
97
154
|
constructor(options?: SubscriberRegistryOptions);
|
|
98
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Number of open — **admitted** — subscribers. A registration still pending
|
|
157
|
+
* admission is counted by `pending_count` instead.
|
|
158
|
+
*/
|
|
99
159
|
get count(): number;
|
|
100
160
|
/**
|
|
101
|
-
*
|
|
161
|
+
* Number of registrations awaiting admission — between `subscribe_pending`
|
|
162
|
+
* and `admit` (or their refusal). For telemetry and tests.
|
|
163
|
+
*/
|
|
164
|
+
get pending_count(): number;
|
|
165
|
+
/**
|
|
166
|
+
* Add a subscriber, admitted at once — the one-step form of
|
|
167
|
+
* `subscribe_pending` + `admit`.
|
|
168
|
+
*
|
|
169
|
+
* **Not for a route that authenticated a revocable credential**: a
|
|
170
|
+
* revocation landing between that route's gates and this call closes
|
|
171
|
+
* nothing, and the stream would open on a dead credential (class doc,
|
|
172
|
+
* "Two-phase registration").
|
|
102
173
|
*
|
|
103
174
|
* @param stream - SSE stream to send data to
|
|
104
175
|
* @param options - channel filter and identity slots (`scope` + `groups`)
|
|
@@ -107,10 +178,42 @@ export declare class SubscriberRegistry<T> {
|
|
|
107
178
|
*/
|
|
108
179
|
subscribe(stream: SseStream<T>, options?: SubscribeOptions): () => void;
|
|
109
180
|
/**
|
|
110
|
-
*
|
|
181
|
+
* Register a subscription **un-admitted** — phase one of the two-phase
|
|
182
|
+
* registration (class doc).
|
|
183
|
+
*
|
|
184
|
+
* The registration is closeable at once by every `close_by_identity`
|
|
185
|
+
* matching its `scope` or `groups`, and until `admit` it receives no
|
|
186
|
+
* broadcast, is not counted, and evicts nothing. A caller that then refuses
|
|
187
|
+
* the request calls the returned handle's `unsubscribe`.
|
|
188
|
+
*
|
|
189
|
+
* @param options - channel filter and identity slots (`scope` + `groups`)
|
|
190
|
+
* @returns the pending handle `admit` takes
|
|
191
|
+
* @mutates registry - adds the un-admitted registration
|
|
192
|
+
*/
|
|
193
|
+
subscribe_pending(options?: SubscribeOptions): PendingSubscription;
|
|
194
|
+
/**
|
|
195
|
+
* Admit a pending subscription, attaching the stream it delivers to — phase
|
|
196
|
+
* two (class doc).
|
|
197
|
+
*
|
|
198
|
+
* If the registration is gone — a revocation closed it while it was
|
|
199
|
+
* pending, or it was unsubscribed — nothing is evicted and the subscription
|
|
200
|
+
* is refused. Otherwise the per-scope cap is enforced as in `subscribe`,
|
|
201
|
+
* counting only admitted subscribers, and the stream starts receiving
|
|
202
|
+
* `broadcast` data. Admitting twice is refused the second time.
|
|
203
|
+
*
|
|
204
|
+
* @param pending - the handle `subscribe_pending` returned
|
|
205
|
+
* @param stream - SSE stream to send data to
|
|
206
|
+
* @returns `false` when the subscription was not admitted — the caller ends the stream
|
|
207
|
+
* @mutates registry - attaches `stream`; closes oldest matching subscribers when `max_per_scope` is exceeded
|
|
208
|
+
*/
|
|
209
|
+
admit(pending: PendingSubscription, stream: SseStream<T>): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Broadcast data to all admitted subscribers on a channel.
|
|
111
212
|
*
|
|
112
213
|
* Subscribers with no channel filter receive all broadcasts.
|
|
113
|
-
* Subscribers with a channel filter only receive matching broadcasts.
|
|
214
|
+
* Subscribers with a channel filter only receive matching broadcasts. A
|
|
215
|
+
* registration pending admission is not an open stream and is skipped —
|
|
216
|
+
* nothing is queued toward a subscriber not yet authorized.
|
|
114
217
|
*/
|
|
115
218
|
broadcast(channel: string, data: T): void;
|
|
116
219
|
/**
|
|
@@ -120,9 +223,17 @@ export declare class SubscriberRegistry<T> {
|
|
|
120
223
|
* Use for auth revocation — when a user's permissions change, close their
|
|
121
224
|
* SSE connections so they must reconnect and re-authenticate.
|
|
122
225
|
*
|
|
226
|
+
* Matches every registration, pending or admitted — never filter on
|
|
227
|
+
* admission here: reaching a registration in flight is the point of the
|
|
228
|
+
* pending phase, and `admit` then refuses it.
|
|
229
|
+
*
|
|
230
|
+
* A stream whose `close` throws does not stop the loop: every match is
|
|
231
|
+
* removed and closed, and the first error is thrown afterward.
|
|
232
|
+
*
|
|
123
233
|
* @param identity - the identity key to match (checked against `scope` and `groups`)
|
|
124
234
|
* @returns the number of subscribers closed
|
|
125
235
|
* @mutates registry - removes matching subscribers and closes their streams
|
|
236
|
+
* @throws the first error a stream's `close` threw, after every match was attempted
|
|
126
237
|
*/
|
|
127
238
|
close_by_identity(identity: string): number;
|
|
128
239
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscriber_registry.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/subscriber_registry.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"subscriber_registry.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/subscriber_registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,0EAA0E;AAC1E,MAAM,WAAW,UAAU,CAAC,CAAC;IAC5B,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACrB,sEAAsE;IACtE,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC7B,mDAAmD;IACnD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qEAAqE;IACrE,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CAC3B;AASD,wCAAwC;AACxC,MAAM,WAAW,yBAAyB;IACzC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAChC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACjC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,WAAW,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,qBAAa,kBAAkB,CAAC,CAAC;;gBAQpB,OAAO,CAAC,EAAE,yBAAyB;IAI/C;;;OAGG;IACH,IAAI,KAAK,IAAI,MAAM,CAMlB;IAED;;;OAGG;IACH,IAAI,aAAa,IAAI,MAAM,CAM1B;IAED;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,IAAI;IASvE;;;;;;;;;;;;OAYG;IACH,iBAAiB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,mBAAmB;IAYlE;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO;IASlE;;;;;;;OAOG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI;IASzC;;;;;;;;;;;;;;;;;;OAkBG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;CAwE3C"}
|