@fuzdev/fuz_app 0.118.0 → 0.120.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) 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 +31 -3
  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 +19 -8
  71. package/dist/db/fact_disk_storage.d.ts +10 -8
  72. package/dist/db/fact_disk_storage.d.ts.map +1 -1
  73. package/dist/db/fact_disk_storage.js +13 -17
  74. package/dist/db/fact_store.d.ts +39 -38
  75. package/dist/db/fact_store.d.ts.map +1 -1
  76. package/dist/db/fact_store.js +48 -86
  77. package/dist/db/file_fact_url.d.ts +6 -6
  78. package/dist/db/file_fact_url.js +6 -6
  79. package/dist/db/status.d.ts +58 -1
  80. package/dist/db/status.d.ts.map +1 -1
  81. package/dist/db/status.js +205 -77
  82. package/dist/http/CLAUDE.md +7 -3
  83. package/dist/http/db_routes.d.ts +6 -2
  84. package/dist/http/db_routes.d.ts.map +1 -1
  85. package/dist/http/db_routes.js +2 -2
  86. package/dist/periodic.d.ts +80 -0
  87. package/dist/periodic.d.ts.map +1 -0
  88. package/dist/periodic.js +122 -0
  89. package/dist/realtime/sse_auth_guard.d.ts +79 -49
  90. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  91. package/dist/realtime/sse_auth_guard.js +103 -73
  92. package/dist/realtime/subscriber_registry.d.ts +119 -8
  93. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  94. package/dist/realtime/subscriber_registry.js +184 -25
  95. package/dist/server/app_backend.d.ts +12 -1
  96. package/dist/server/app_backend.d.ts.map +1 -1
  97. package/dist/server/app_backend.js +5 -2
  98. package/dist/server/app_server.d.ts +55 -15
  99. package/dist/server/app_server.d.ts.map +1 -1
  100. package/dist/server/app_server.js +41 -13
  101. package/dist/server/serve_fact_route.d.ts +10 -7
  102. package/dist/server/serve_fact_route.d.ts.map +1 -1
  103. package/dist/server/serve_fact_route.js +11 -8
  104. package/dist/testing/CLAUDE.md +140 -48
  105. package/dist/testing/app_server.d.ts +2 -2
  106. package/dist/testing/app_server.d.ts.map +1 -1
  107. package/dist/testing/app_server.js +11 -2
  108. package/dist/testing/audit_drift_guard.d.ts +1 -48
  109. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  110. package/dist/testing/audit_drift_guard.js +0 -42
  111. package/dist/testing/connection_closer_helpers.d.ts +9 -22
  112. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  113. package/dist/testing/connection_closer_helpers.js +11 -17
  114. package/dist/testing/cross_backend/capabilities.d.ts +67 -13
  115. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  116. package/dist/testing/cross_backend/capabilities.js +27 -12
  117. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  118. package/dist/testing/cross_backend/default_backend_configs.js +21 -0
  119. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  120. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
  121. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  122. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
  123. package/dist/testing/cross_backend/session_cap.d.ts +13 -0
  124. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  125. package/dist/testing/cross_backend/session_cap.js +55 -19
  126. package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
  127. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/sse_round_trip.js +93 -26
  129. package/dist/testing/cross_backend/testing_server_bun.js +4 -4
  130. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  131. package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
  132. package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
  133. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
  135. package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
  136. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  137. package/dist/testing/cross_backend/ws_round_trip.js +156 -19
  138. package/dist/testing/sse_round_trip.d.ts +6 -4
  139. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  140. package/dist/testing/stubs.d.ts +1 -1
  141. package/dist/testing/stubs.d.ts.map +1 -1
  142. package/dist/testing/stubs.js +6 -4
  143. package/dist/testing/transports/sse_transport.d.ts +1 -1
  144. package/dist/testing/transports/ws_client.d.ts +7 -0
  145. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  146. package/dist/testing/transports/ws_raw_client.d.ts +51 -0
  147. package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
  148. package/dist/testing/transports/ws_raw_client.js +156 -0
  149. package/dist/testing/transports/ws_transport.d.ts +28 -0
  150. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  151. package/dist/testing/transports/ws_transport.js +45 -1
  152. package/dist/testing/ws_round_trip.d.ts +8 -3
  153. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  154. package/dist/testing/ws_round_trip.js +19 -4
  155. package/dist/ui/AdminAccounts.svelte +4 -3
  156. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  157. package/dist/ui/AdminOverview.svelte +3 -1
  158. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  159. package/dist/ui/CLAUDE.md +22 -6
  160. package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
  161. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  162. package/dist/ui/admin_accounts_state.svelte.js +16 -0
  163. package/dist/ui/auth_state.svelte.d.ts +25 -1
  164. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  165. package/dist/ui/auth_state.svelte.js +28 -1
  166. package/package.json +2 -2
  167. package/dist/server/file_fact_fetcher.d.ts +0 -42
  168. package/dist/server/file_fact_fetcher.d.ts.map +0 -1
  169. package/dist/server/file_fact_fetcher.js +0 -60
@@ -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
- * `create_sse_auth_guard` bridges audit events to `SubscriberRegistry.close_by_identity()`,
5
- * closing SSE streams when a subscriber's access is revoked (role revocation or
6
- * session invalidation).
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 consumers.
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 { SubscriberRegistry, type SubscribeOptions } from './subscriber_registry.js';
16
- import type { SseStream, SseNotification, EventSpec } from './sse.js';
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
- * Audit event types that trigger SSE stream disconnection — the union of
21
- * access-invalidation events. Over-closing a one-way admin feed is cheap (the
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
- * Closes streams when:
49
- * - `role_grant_revoke` fires for the `required_role` targeting a connected subscriber
50
- * - `session_revoke` targets the specific revoked session (session-hash-scoped)
51
- * - `session_revoke_all` / `token_revoke_all` / `password_change` / `logout`
52
- * target a connected subscriber (account-wide)
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
- * The registry must use `account_id` as the identity key when subscribing
55
- * (passed as the third argument to `registry.subscribe()`).
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
- /** Subscribe function — pass as part of `stream` option to `create_audit_log_route_specs`. */
73
- subscribe: (stream: SseStream<SseNotification>, options?: SubscribeOptions) => () => void;
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 and auth guard.
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. The result satisfies `AuditLogRouteOptions['stream']`
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 audit_sse = create_audit_log_sse({log});
137
+ * const connection_closer = create_realtime_closer();
138
+ * const audit_sse = create_audit_log_sse({log, connection_closer});
120
139
  *
121
- * // Inside the audit_factory body on CreateAppBackendOptions:
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;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEtE,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAOrD,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;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,CA6CjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC1F,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,yJAAyJ;IACzJ,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
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
- * `create_sse_auth_guard` bridges audit events to `SubscriberRegistry.close_by_identity()`,
5
- * closing SSE streams when a subscriber's access is revoked (role revocation or
6
- * session invalidation).
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 consumers.
17
+ * guard, closer, and broadcaster — making the secure path the easy path for
18
+ * consumers.
10
19
  *
11
20
  * @module
12
21
  */
13
- import { AUDIT_EVENT_TYPES, AuditLogEventJson } from '../auth/audit_log_schema.js';
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
- * Audit event types that trigger SSE stream disconnection — the union of
19
- * access-invalidation events. Over-closing a one-way admin feed is cheap (the
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
- * Closes streams when:
54
- * - `role_grant_revoke` fires for the `required_role` targeting a connected subscriber
55
- * - `session_revoke` targets the specific revoked session (session-hash-scoped)
56
- * - `session_revoke_all` / `token_revoke_all` / `password_change` / `logout`
57
- * target a connected subscriber (account-wide)
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
- * The registry must use `account_id` as the identity key when subscribing
60
- * (passed as the third argument to `registry.subscribe()`).
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
- // session_revoke is session-scoped, not account-scoped — close only the
81
- // stream subscribed under the revoked session's hash. The hash is already
82
- // in the event metadata (set by the `account_session_revoke` RPC handler).
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
- const closed = registry.close_by_identity(session_id);
88
- if (closed > 0) {
89
- log.info(`SSE auth guard: closed ${closed} stream(s) for session ${session_id} (session_revoke)`);
90
- }
91
- return;
92
- }
93
- // role_grant_revoke requires matching the specific role. `null` means the
94
- // stream isn't gated by a specific role_grant, so role_grant_revoke is a no-op.
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
- if (event.metadata?.role !== required_role)
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
- // resolve the affected account — admin actions set target_account_id,
102
- // self-service actions (password_change, own session_revoke_all) only set account_id
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 and auth guard.
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. The result satisfies `AuditLogRouteOptions['stream']`
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 audit_sse = create_audit_log_sse({log});
182
+ * const connection_closer = create_realtime_closer();
183
+ * const audit_sse = create_audit_log_sse({log, connection_closer});
156
184
  *
157
- * // Inside the audit_factory body on CreateAppBackendOptions:
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`. On subscribe, when the count
36
- * of subscribers with the same `scope` reaches this limit, the oldest
37
- * matching subscriber(s) are closed before the new one is added.
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
- /** Number of active subscribers. */
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
- * Add a subscriber.
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
- * Broadcast data to all subscribers on a channel.
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;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,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;AAED,wCAAwC;AACxC,MAAM,WAAW,yBAAyB;IACzC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,kDAAkD;AAClD,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,qBAAa,kBAAkB,CAAC,CAAC;;gBAIpB,OAAO,CAAC,EAAE,yBAAyB;IAI/C,oCAAoC;IACpC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,IAAI;IAmBvE;;;;;OAKG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI;IAQzC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;CAiC3C"}
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"}