@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
@@ -12,10 +12,12 @@ import '../assert_dev_env.js';
12
12
  * cross-process bundle (`describe_standard_cross_process_tests`) omits WS
13
13
  * by design, so consumers call this alongside it.
14
14
  *
15
- * **Consumer-agnostic.** Every case drives the `heartbeat` protocol action,
16
- * which `assert_ws_endpoints_include_protocol_actions` guarantees is present
17
- * on every WS endpoint — so the suite needs no knowledge of a consumer's
18
- * domain WS methods. It validates the transport, not the domain.
15
+ * **Consumer-agnostic**, apart from the cases `rpc_path` and
16
+ * `capabilities.ws_account_actions` gate. Every other case drives the
17
+ * `heartbeat` protocol action, which
18
+ * `assert_ws_endpoints_include_protocol_actions` guarantees is present on
19
+ * every WS endpoint — so the suite needs no knowledge of a consumer's domain
20
+ * WS methods. It validates the transport, not the domain.
19
21
  *
20
22
  * The first three cases mirror the upgrade stack `register_ws_endpoint` wires
21
23
  * (origin check → `require_auth` → dispatch): an authenticated upgrade
@@ -24,20 +26,36 @@ import '../assert_dev_env.js';
24
26
  * upgrade time (not per message), so the negative cases assert the upgrade
25
27
  * itself rejects rather than a per-message error frame.
26
28
  *
27
- * Two spine-parity cases follow: `heartbeat` answers the parameterless shapes
28
- * (absent, `{}`) and refuses a declared param with `invalid_params`, and a
29
+ * Spine-parity cases follow. Both spines admit a connection only after
30
+ * re-reading its credential, and requests that arrive first wait for that
31
+ * rather than being dropped: requests sent the moment the socket opens are all
32
+ * answered, and — gated on `capabilities.ws_handshake_pipelining`, with the
33
+ * ordering forced — so are frames written in the same TCP write as the upgrade
34
+ * request (driven through `connect_raw_ws`, since a client library sends
35
+ * nothing before its `open` event). `heartbeat` answers the parameterless
36
+ * shapes (absent, `{}`) and refuses a declared param with `invalid_params`, and a
29
37
  * message over the backend's cap (`max_message_bytes`, default
30
38
  * `DEFAULT_WS_MAX_MESSAGE_BYTES`) closes the socket with
31
39
  * `WS_CLOSE_MESSAGE_TOO_BIG`.
32
40
  *
33
- * A final case (gated on `rpc_path`) covers server-initiated close: an
34
- * authenticated socket is dropped when the account's sessions are revoked
35
- * mid-connection. Per-message dispatch never re-checks credential validity,
36
- * so the live socket survives on the audit-fed `create_ws_auth_guard` seam —
37
- * firing `account_session_revoke_all` over the keeper's session channel
38
- * emits `session_revoke_all`, which closes the socket. Omit `rpc_path` to
39
- * skip it (consumers without the standard account actions on their RPC
40
- * endpoint).
41
+ * The final cases (gated on `rpc_path`) cover server-initiated close.
42
+ * Per-message dispatch never re-checks credential validity, so an open socket
43
+ * ends only when a revocation closes it — which every revocation does, once
44
+ * its transaction commits. An authenticated socket is dropped when the
45
+ * account's sessions are revoked over the keeper's session channel
46
+ * (`account_session_revoke_all`), and when its account is deleted
47
+ * (`account_delete`, also gated on `capabilities.account_lifecycle`). Omit
48
+ * `rpc_path` to skip them (consumers without the standard account and admin
49
+ * actions on their RPC endpoint).
50
+ *
51
+ * One more case is gated on `capabilities.ws_account_actions` — the backend's
52
+ * WS endpoint mounts the self-service account actions: a socket that revokes
53
+ * its own sessions *over that socket* is closed with
54
+ * `WS_CLOSE_SESSION_REVOKED`, and — on a backend declaring
55
+ * `capabilities.ws_self_revocation_reply` — reads the reply to its request
56
+ * first. The TS spine queues the close behind the commit and sends the reply
57
+ * ahead of it, so the caller is never left without an answer; the Rust spine
58
+ * does not answer yet, which that flag records.
41
59
  *
42
60
  * Gated on `capabilities.ws` — backends without an end-to-end WS transport
43
61
  * skip (the cases still surface as `.skip` in the report). Cross-process
@@ -49,11 +67,13 @@ import '../assert_dev_env.js';
49
67
  import { assert, describe } from 'vitest';
50
68
  import { assert_rejects } from '@fuzdev/fuz_util/testing.ts';
51
69
  import { heartbeat_action_spec } from '../../actions/heartbeat.js';
52
- import { DEFAULT_WS_MAX_MESSAGE_BYTES, WS_CLOSE_MESSAGE_TOO_BIG } from '../../actions/transports.js';
70
+ import { DEFAULT_WS_MAX_MESSAGE_BYTES, WS_CLOSE_MESSAGE_TOO_BIG, WS_CLOSE_SESSION_REVOKED } from '../../actions/transports.js';
53
71
  import { account_session_revoke_all_action_spec } from '../../auth/account_action_specs.js';
72
+ import { account_delete_action_spec } from '../../auth/admin_action_specs.js';
54
73
  import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
55
74
  import { is_response_for } from '../transports/ws_client.js';
56
75
  import { create_ws_transport } from '../transports/ws_transport.js';
76
+ import { connect_raw_ws, is_raw_ws_close, RAW_WS_OPCODE_TEXT } from '../transports/ws_raw_client.js';
57
77
  import { create_rpc_post_init } from '../rpc_helpers.js';
58
78
  import { test_if } from './capabilities.js';
59
79
  /** Origin guaranteed to fail the `http://localhost:*` allowlist the test backends run with. */
@@ -63,7 +83,8 @@ const DISALLOWED_ORIGIN = 'http://disallowed.example';
63
83
  * `heartbeat` round-trip, `heartbeat`'s parameterless-shape contract, an
64
84
  * oversized message closing with `WS_CLOSE_MESSAGE_TOO_BIG`, anonymous-upgrade
65
85
  * refusal, disallowed-origin refusal, and — when `rpc_path` is supplied —
66
- * session-revocation closing the live socket.
86
+ * session revocation and account deletion closing the live socket, plus a
87
+ * self-revoking socket reading its reply before its close.
67
88
  */
68
89
  export const describe_cross_process_ws_tests = (options) => {
69
90
  const { setup_test, capabilities, base_url, ws_path, origin, rpc_path, max_message_bytes = DEFAULT_WS_MAX_MESSAGE_BYTES } = options;
@@ -82,6 +103,61 @@ export const describe_cross_process_ws_tests = (options) => {
82
103
  await client.close();
83
104
  }
84
105
  });
106
+ // Both spines answer the handshake before the connection is admitted
107
+ // (the credential is re-read in between) and dispatch nothing until it
108
+ // is. Requests that reach the backend first must wait for that
109
+ // admission — not be dropped by it, and not run ahead of it.
110
+ test_if(capabilities.ws, 'requests sent the moment the socket opens are all answered', async () => {
111
+ const client = await open_authed();
112
+ try {
113
+ const ids = [1, 2, 3];
114
+ // back to back, with no round trip first — these may reach the
115
+ // backend while the connection is still pending admission
116
+ await Promise.all(ids.map((id) => client.send({ jsonrpc: '2.0', id, method: heartbeat_action_spec.method })));
117
+ const frames = await Promise.all(ids.map((id) => client.wait_for(is_response_for(id))));
118
+ for (const frame of frames) {
119
+ assert.ok('result' in frame, JSON.stringify(frame));
120
+ assert.deepStrictEqual(frame.result, {});
121
+ }
122
+ assert.strictEqual(client.close_code, null);
123
+ }
124
+ finally {
125
+ await client.close();
126
+ }
127
+ });
128
+ // The same contract with the ordering forced: frames written in the same
129
+ // TCP write as the upgrade request are in the backend's hands before it
130
+ // can have admitted anything.
131
+ test_if(capabilities.ws && capabilities.ws_handshake_pipelining, 'frames sent with the handshake are answered once the connection is admitted', async () => {
132
+ const fixture = await setup_test();
133
+ const url = new URL(base_url);
134
+ const ids = [1, 2, 3];
135
+ const client = await connect_raw_ws({
136
+ hostname: url.hostname,
137
+ port: url.port ? Number(url.port) : 80,
138
+ path: ws_path,
139
+ headers: {
140
+ Cookie: fixture.transport.cookies().join('; '),
141
+ Origin: origin ?? base_url
142
+ },
143
+ pipelined: ids.map((id) => JSON.stringify({ jsonrpc: '2.0', id, method: heartbeat_action_spec.method }))
144
+ });
145
+ try {
146
+ for (const id of ids) {
147
+ const frame = await client.wait_for((f) => f.opcode === RAW_WS_OPCODE_TEXT &&
148
+ JSON.parse(f.payload.toString('utf-8')).id === id);
149
+ assert.deepStrictEqual(JSON.parse(frame.payload.toString('utf-8')), {
150
+ jsonrpc: '2.0',
151
+ id,
152
+ result: {}
153
+ });
154
+ }
155
+ assert.ok(!client.frames.some(is_raw_ws_close), 'the socket stays open');
156
+ }
157
+ finally {
158
+ client.destroy();
159
+ }
160
+ });
85
161
  // `heartbeat` is parameterless on both spines (`z.void()` /
86
162
  // `require_void_params`): the empty shapes are the no-arg call, and a
87
163
  // declared param is `invalid_params`, not ignored.
@@ -146,9 +222,9 @@ export const describe_cross_process_ws_tests = (options) => {
146
222
  await assert_rejects(() => create_ws_transport({ base_url, ws_path, cookies: [], origin: DISALLOWED_ORIGIN }));
147
223
  });
148
224
  // Per-message dispatch never re-checks credential validity, so a live
149
- // socket only drops via the audit-fed `create_ws_auth_guard`. Revoke the
150
- // keeper's sessions over its own session channel → `session_revoke_all`
151
- // closes the socket. Gated on `rpc_path` (depends on the standard
225
+ // socket drops only when a revocation closes it. Revoke the keeper's
226
+ // sessions over its own session channel → the handler's post-commit
227
+ // close ends the socket. Gated on `rpc_path` (depends on the standard
152
228
  // account actions on the RPC endpoint).
153
229
  test_if(capabilities.ws && rpc_path !== undefined, 'session revocation closes the live socket', async () => {
154
230
  const fixture = await setup_test();
@@ -171,5 +247,66 @@ export const describe_cross_process_ws_tests = (options) => {
171
247
  await client.close();
172
248
  }
173
249
  });
250
+ // A soft-deleted account's credentials stop authenticating, and its open
251
+ // socket must end with them: the tombstone revokes every session and
252
+ // token, and the handler closes the account's connections once that has
253
+ // committed. The keeper deletes a second account whose socket is open.
254
+ test_if(capabilities.ws && capabilities.account_lifecycle && rpc_path !== undefined, 'an account delete closes the live socket of the deleted account', async () => {
255
+ const fixture = await setup_test();
256
+ const target = await fixture.create_account({ username: 'ws_delete_target', roles: [] });
257
+ const cookie = target.create_session_headers().cookie;
258
+ assert.ok(cookie, 'expected a session cookie for the target account');
259
+ const client = await create_ws_transport({ base_url, ws_path, cookies: [cookie], origin });
260
+ try {
261
+ // admitted and dispatching before the delete, so a failed close
262
+ // assertion can't be confused with a dead connection
263
+ await client.request(1, heartbeat_action_spec.method, {});
264
+ const res = await fixture.transport(rpc_path, create_rpc_post_init(account_delete_action_spec.method, {
265
+ account_id: target.account.id
266
+ }));
267
+ assert.strictEqual(res.status, 200, `account_delete RPC failed (status=${res.status})`);
268
+ const closed = await client.wait_for_close(2000);
269
+ assert.ok(closed, 'socket did not close within 2s after account_delete');
270
+ assert.strictEqual(client.close_code, WS_CLOSE_SESSION_REVOKED);
271
+ }
272
+ finally {
273
+ await client.close();
274
+ }
275
+ });
276
+ // A socket that revokes the session it is running on. It must be closed;
277
+ // and where the backend declares `ws_self_revocation_reply`, the request
278
+ // is answered first, the close following from behind the revocation's
279
+ // commit. A caller left with only the close cannot tell a completed
280
+ // revocation from a dropped connection.
281
+ test_if(capabilities.ws && capabilities.ws_account_actions, capabilities.ws_self_revocation_reply
282
+ ? 'a socket that revokes its own sessions reads the reply, then the revocation close'
283
+ : 'a socket that revokes its own sessions is closed with the revocation code', async () => {
284
+ const client = await open_authed();
285
+ try {
286
+ await client.request(1, heartbeat_action_spec.method, {});
287
+ const revoke_id = 2;
288
+ await client.send({
289
+ jsonrpc: '2.0',
290
+ id: revoke_id,
291
+ method: account_session_revoke_all_action_spec.method
292
+ });
293
+ const closed = await client.wait_for_close(2000);
294
+ assert.ok(closed, 'socket did not close within 2s of revoking its own sessions');
295
+ assert.strictEqual(client.close_code, WS_CLOSE_SESSION_REVOKED);
296
+ if (capabilities.ws_self_revocation_reply) {
297
+ // frames arrive in wire order and nothing arrives after the
298
+ // close, so a reply in hand was written ahead of it
299
+ const reply = client.messages.find(is_response_for(revoke_id));
300
+ assert.ok(reply, 'the reply must reach the caller before the close');
301
+ assert.ok('result' in reply, JSON.stringify(reply));
302
+ const result = reply.result;
303
+ assert.strictEqual(result.ok, true);
304
+ assert.ok((result.count ?? 0) >= 1, "at least the socket's own session was revoked");
305
+ }
306
+ }
307
+ finally {
308
+ await client.close();
309
+ }
310
+ });
174
311
  });
175
312
  };
@@ -27,8 +27,9 @@ export interface SseRouteTestSpec {
27
27
  event_specs?: Array<EventSpec>;
28
28
  /**
29
29
  * Whether to assert the stream closes after `session_revoke_all`.
30
- * Default `true`. Set `false` for endpoints that don't wire a close-on-revoke
31
- * guard (leaves a TODO to fix, rather than silently passing).
30
+ * Default `true`. Set `false` for endpoints whose registry no revocation
31
+ * reaches — neither joined to the backend's closer nor guarded (leaves a
32
+ * TODO to fix, rather than silently passing).
32
33
  */
33
34
  assert_closes_on_revoke?: boolean;
34
35
  }
@@ -45,8 +46,9 @@ export interface SseRouteTestOptions {
45
46
  /**
46
47
  * Backend audit event callback — threaded to `create_test_app_server`.
47
48
  * Use to wire a close-on-revoke guard for consumer SSE registries
48
- * (e.g., via `create_sse_auth_guard`) so `session_revoke_all` actually
49
- * closes the tested streams.
49
+ * (e.g., via `create_sse_auth_guard`) so `session_revoke_all` closes the
50
+ * tested streams — not needed for a registry the route joined to
51
+ * `ctx.deps.connection_closer`, which the revoking handler closes itself.
50
52
  */
51
53
  on_audit_event?: (event: AuditLogEvent) => void;
52
54
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAmB7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,oBAAoB,CAAC;AAGrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAGjE,OAAO,EAEN,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAEhE,OAAO,EAIN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAO1B,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAChC,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E;;;OAGG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC/B;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IACnC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,qDAAqD;IACrD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC,8BAA8B;IAC9B,MAAM,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;CAChC;AAmDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,mBAAmB,KAAG,IAkIvE,CAAC"}
1
+ {"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAmB7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,oBAAoB,CAAC;AAGrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAGjE,OAAO,EAEN,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAEhE,OAAO,EAIN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAO1B,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAChC,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E;;;OAGG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IACnC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,qDAAqD;IACrD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC,8BAA8B;IAC9B,MAAM,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;CAChC;AAmDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,mBAAmB,KAAG,IAkIvE,CAAC"}
@@ -71,7 +71,7 @@ export declare const create_test_audit_emitter: () => AuditEmitter;
71
71
  * Build a no-op `AuditLogSse` for tests that wire `audit_sse` into the
72
72
  * surface helper but don't assert on SSE fan-out or subscriber state.
73
73
  *
74
- * `subscribe` returns a no-op cleanup; `on_audit_event` is a no-op; the
74
+ * `on_audit_event` is a no-op, so nothing is broadcast; the
75
75
  * `registry` is a fresh `SubscriberRegistry` instance (call sites that
76
76
  * inspect `.size` or call `.close_*` see a real registry, so writes are
77
77
  * isolated per test). Tests that need real SSE plumbing build it via
@@ -1 +1 @@
1
- {"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAa7B,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAG3E,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAI9F;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,GAAG,GAAG,EAAE,OAAO,MAAM,KAAG,CAqBtD,CAAC;AAET;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,GAAG,GAAG,EAAE,QAAQ,MAAM,EAAE,YAAY,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,CAOxF,CAAC;AAET,iEAAiE;AACjE,eAAO,MAAM,IAAI,EAAE,GAAkC,CAAC;AAEtD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,QAAO,EAI/B,CAAC;AAEJ,gDAAgD;AAChD,eAAO,MAAM,YAAY,QAAO,QAAgC,CAAC;AAEjE,2CAA2C;AAC3C,eAAO,MAAM,OAAO,GAAU,IAAI,GAAG,EAAE,MAAM,GAAG,KAAG,OAAO,CAAC,IAAI,CAAW,CAAC;AAI3E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,QAAO,YAW3C,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,QAAO,WASxC,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,aAAa,EAAE,OAS3B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,QAAO,OAWtC,CAAC;AAEH,2FAA2F;AAC3F,eAAO,MAAM,0BAA0B,GAAI,UAAU;IACpD,iDAAiD;IACjD,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAC/B,KAAG,KAAK,CAAC,cAAc,CAqBvB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,GAC1C,iBAAiB,cAAc,CAAC,MAAM,CAAC,KACrC,gBAsBF,CAAC;AAEF,kDAAkD;AAClD,MAAM,WAAW,+BAA+B;IAC/C,6DAA6D;IAC7D,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,qFAAqF;IACrF,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,oEAAoE;IACpE,UAAU,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACzB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC/B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;IAC7F;;;;;;;;OAQG;IACH,YAAY,CAAC,EACZ,aAAa,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,gBAAgB,KAAK,aAAa,CAAC,cAAc,CAAC,CAAC,CAAC;IAC5F,mFAAmF;IACnF,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,KAAK,CAAC,cAAc,CAAC,CAAC;IAC/E;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,sBAAsB,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,cAmDF,CAAC"}
@@ -9,6 +9,7 @@ import './assert_dev_env.js';
9
9
  */
10
10
  import { Logger } from '@fuzdev/fuz_util/log.ts';
11
11
  import { ApiError, RateLimitError } from '../http/error_schemas.js';
12
+ import { create_realtime_closer } from '../actions/connection_closer.js';
12
13
  import { Db } from '../db/db.js';
13
14
  import { prefix_route_specs } from '../http/route_spec.js';
14
15
  import { bootstrap_route_shape } from '../auth/bootstrap_route_schema.js';
@@ -114,7 +115,7 @@ export const create_test_audit_emitter = () => ({
114
115
  * Build a no-op `AuditLogSse` for tests that wire `audit_sse` into the
115
116
  * surface helper but don't assert on SSE fan-out or subscriber state.
116
117
  *
117
- * `subscribe` returns a no-op cleanup; `on_audit_event` is a no-op; the
118
+ * `on_audit_event` is a no-op, so nothing is broadcast; the
118
119
  * `registry` is a fresh `SubscriberRegistry` instance (call sites that
119
120
  * inspect `.size` or call `.close_*` see a real registry, so writes are
120
121
  * isolated per test). Tests that need real SSE plumbing build it via
@@ -125,7 +126,6 @@ export const create_stub_audit_sse = () => {
125
126
  max_per_scope: AUDIT_LOG_SSE_MAX_PER_SCOPE
126
127
  });
127
128
  return {
128
- subscribe: () => () => { },
129
129
  log: new Logger('test:audit_sse', { level: 'off' }),
130
130
  on_audit_event: () => { },
131
131
  registry
@@ -139,7 +139,8 @@ export const stub_app_deps = {
139
139
  password: create_throwing_stub('password'),
140
140
  db: create_throwing_stub('db'),
141
141
  log: create_throwing_stub('log'),
142
- audit: create_test_audit_emitter()
142
+ audit: create_test_audit_emitter(),
143
+ connection_closer: create_realtime_closer()
143
144
  };
144
145
  /**
145
146
  * Create no-op `AppDeps` for auth surface testing.
@@ -153,7 +154,8 @@ export const create_stub_app_deps = () => ({
153
154
  password: create_noop_stub('password'),
154
155
  db: stub_db,
155
156
  log: new Logger('test', { level: 'off' }),
156
- audit: create_test_audit_emitter()
157
+ audit: create_test_audit_emitter(),
158
+ connection_closer: create_realtime_closer()
157
159
  });
158
160
  /** Create the API middleware stub array matching `create_auth_middleware_specs` output. */
159
161
  export const create_stub_api_middleware = (options) => {
@@ -33,7 +33,7 @@ export interface SseTransport {
33
33
  /**
34
34
  * Drain until the server closes the stream. Resolves `true` if the
35
35
  * stream closes within `timeout_ms`, `false` on timeout. The signal for
36
- * an auth-guard revocation dropping a live stream — mirrors
36
+ * a revocation dropping a live stream — mirrors
37
37
  * `WsClient.wait_for_close`.
38
38
  */
39
39
  wait_for_close: (timeout_ms?: number) => Promise<boolean>;
@@ -130,6 +130,13 @@ export interface WsClient {
130
130
  * connection dropped without a close frame. `null` while open.
131
131
  */
132
132
  readonly close_code: number | null;
133
+ /**
134
+ * The close reason of the connection once it has closed — the text sent
135
+ * with `close_code` (e.g. `'connection limit'` with
136
+ * `WS_CLOSE_CONNECTION_LIMIT`), empty when the close carried none. `null`
137
+ * while open.
138
+ */
139
+ readonly close_reason: string | null;
133
140
  /** Every message the server has sent, in arrival order. */
134
141
  readonly messages: ReadonlyArray<unknown>;
135
142
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ws_client.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws_client.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import '../assert_dev_env.js';
2
+ /** RFC 6455 opcode of a text frame. */
3
+ export declare const RAW_WS_OPCODE_TEXT = 1;
4
+ /** RFC 6455 opcode of a close frame. */
5
+ export declare const RAW_WS_OPCODE_CLOSE = 8;
6
+ /** One frame the server sent. */
7
+ export interface RawWsFrame {
8
+ opcode: number;
9
+ payload: Buffer;
10
+ }
11
+ /** A WebSocket client that sends frames and never answers a close. */
12
+ export interface RawWsClient {
13
+ /** Every frame the server sent, in order. */
14
+ frames: Array<RawWsFrame>;
15
+ /** Send one masked text frame. */
16
+ send_text: (text: string) => void;
17
+ /** Resolves with the first frame, already received or not, matching `predicate`. */
18
+ wait_for: (predicate: (frame: RawWsFrame) => boolean, timeout_ms?: number) => Promise<RawWsFrame>;
19
+ /** Drop the TCP connection, with no close handshake. */
20
+ destroy: () => void;
21
+ }
22
+ /** Options for `connect_raw_ws`. */
23
+ export interface RawWsConnectOptions {
24
+ port: number;
25
+ /** @default '127.0.0.1' */
26
+ hostname?: string;
27
+ /** The WebSocket endpoint path (e.g. `/api/ws`). */
28
+ path: string;
29
+ /** Extra upgrade request headers — `Cookie`, `Origin`. */
30
+ headers?: Record<string, string>;
31
+ /**
32
+ * Text frames to write in the **same TCP write** as the upgrade request, so
33
+ * they are in the server's hands before the handshake is answered — and so
34
+ * before the connection can have been admitted.
35
+ */
36
+ pipelined?: ReadonlyArray<string>;
37
+ }
38
+ /** Whether a frame is a close frame. */
39
+ export declare const is_raw_ws_close: (frame: RawWsFrame) => boolean;
40
+ /** The close code a close frame carries, or `null` when it carries none. */
41
+ export declare const raw_ws_close_code: (frame: RawWsFrame) => number | null;
42
+ /**
43
+ * Upgrade a raw TCP socket to a WebSocket.
44
+ *
45
+ * Resolves once the server answers `101`; rejects on any other status or a
46
+ * connection error.
47
+ *
48
+ * @throws Error when the upgrade is refused or the connection fails
49
+ */
50
+ export declare const connect_raw_ws: (options: RawWsConnectOptions) => Promise<RawWsClient>;
51
+ //# sourceMappingURL=ws_raw_client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws_raw_client.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_raw_client.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,uCAAuC;AACvC,eAAO,MAAM,kBAAkB,IAAM,CAAC;AACtC,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,IAAM,CAAC;AAEvC,iCAAiC;AACjC,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC3B,6CAA6C;IAC7C,MAAM,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAC1B,kCAAkC;IAClC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,oFAAoF;IACpF,QAAQ,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAClG,wDAAwD;IACxD,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,oCAAoC;AACpC,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;OAIG;IACH,SAAS,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,wCAAwC;AACxC,eAAO,MAAM,eAAe,GAAI,OAAO,UAAU,KAAG,OAA+C,CAAC;AAEpG,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB,GAAI,OAAO,UAAU,KAAG,MAAM,GAAG,IACE,CAAC;AAwClE;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,mBAAmB,KAAG,OAAO,CAAC,WAAW,CAoF9E,CAAC"}
@@ -0,0 +1,156 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * A raw-socket WebSocket client for tests that need to misbehave, or to
4
+ * control exactly which bytes reach the server when.
5
+ *
6
+ * A client library answers a close frame itself and sends nothing until its
7
+ * `open` event, so two server behaviors can't be reached through one: what
8
+ * the server does with a client that **withholds its close frame**, and what
9
+ * it does with frames that arrive **with the handshake**, before the
10
+ * connection can have been admitted. This speaks just enough RFC 6455 over
11
+ * `node:net` to do both — the upgrade request, masked client text frames, and
12
+ * unmasked server frames — and deliberately never replies to a close.
13
+ *
14
+ * Small frames only (payloads under 64 KiB, unfragmented), which is all the
15
+ * JSON-RPC traffic the suites send.
16
+ *
17
+ * @module
18
+ */
19
+ import { randomBytes } from 'node:crypto';
20
+ import { connect } from 'node:net';
21
+ /** RFC 6455 opcode of a text frame. */
22
+ export const RAW_WS_OPCODE_TEXT = 0x1;
23
+ /** RFC 6455 opcode of a close frame. */
24
+ export const RAW_WS_OPCODE_CLOSE = 0x8;
25
+ /** Whether a frame is a close frame. */
26
+ export const is_raw_ws_close = (frame) => frame.opcode === RAW_WS_OPCODE_CLOSE;
27
+ /** The close code a close frame carries, or `null` when it carries none. */
28
+ export const raw_ws_close_code = (frame) => frame.payload.length >= 2 ? frame.payload.readUInt16BE(0) : null;
29
+ /** Encode one masked client text frame. */
30
+ const encode_client_text = (text) => {
31
+ const payload = Buffer.from(text, 'utf-8');
32
+ if (payload.length >= 65_536)
33
+ throw new Error('raw ws: payload too large for this test client');
34
+ const first = 0x80 | RAW_WS_OPCODE_TEXT;
35
+ const header = payload.length < 126
36
+ ? Buffer.from([first, 0x80 | payload.length])
37
+ : Buffer.from([first, 0x80 | 126, payload.length >> 8, payload.length & 0xff]);
38
+ const mask = randomBytes(4);
39
+ const masked = Buffer.from(payload);
40
+ for (let i = 0; i < masked.length; i++)
41
+ masked[i] ^= mask[i % 4];
42
+ return Buffer.concat([header, mask, masked]);
43
+ };
44
+ /** Split complete server frames off the front of `buffer` (server frames are unmasked). */
45
+ const take_frames = (buffer) => {
46
+ const frames = [];
47
+ let offset = 0;
48
+ for (;;) {
49
+ if (buffer.length - offset < 2)
50
+ break;
51
+ const opcode = buffer[offset] & 0x0f;
52
+ let length = buffer[offset + 1] & 0x7f;
53
+ let header = 2;
54
+ if (length === 126) {
55
+ if (buffer.length - offset < 4)
56
+ break;
57
+ length = buffer.readUInt16BE(offset + 2);
58
+ header = 4;
59
+ }
60
+ else if (length === 127) {
61
+ throw new Error('raw ws: 64-bit frame lengths are not supported by this test client');
62
+ }
63
+ if (buffer.length - offset < header + length)
64
+ break;
65
+ frames.push({ opcode, payload: buffer.subarray(offset + header, offset + header + length) });
66
+ offset += header + length;
67
+ }
68
+ return { frames, rest: buffer.subarray(offset) };
69
+ };
70
+ /**
71
+ * Upgrade a raw TCP socket to a WebSocket.
72
+ *
73
+ * Resolves once the server answers `101`; rejects on any other status or a
74
+ * connection error.
75
+ *
76
+ * @throws Error when the upgrade is refused or the connection fails
77
+ */
78
+ export const connect_raw_ws = (options) => new Promise((resolve, reject) => {
79
+ const { port, hostname = '127.0.0.1', path, headers = {}, pipelined = [] } = options;
80
+ const socket = connect(port, hostname);
81
+ const frames = [];
82
+ const waiters = [];
83
+ let buffer = Buffer.alloc(0);
84
+ let upgraded = false;
85
+ const client = {
86
+ frames,
87
+ send_text: (text) => {
88
+ socket.write(encode_client_text(text));
89
+ },
90
+ wait_for: (predicate, timeout_ms = 2000) => {
91
+ const found = frames.find(predicate);
92
+ if (found)
93
+ return Promise.resolve(found);
94
+ return new Promise((resolve_frame, reject_frame) => {
95
+ const waiter = {
96
+ predicate,
97
+ resolve: (frame) => {
98
+ clearTimeout(timer);
99
+ resolve_frame(frame);
100
+ }
101
+ };
102
+ const timer = setTimeout(() => {
103
+ waiters.splice(waiters.indexOf(waiter), 1);
104
+ reject_frame(new Error(`raw ws: no matching frame within ${timeout_ms}ms`));
105
+ }, timeout_ms);
106
+ waiters.push(waiter);
107
+ });
108
+ },
109
+ destroy: () => socket.destroy()
110
+ };
111
+ socket.on('error', reject);
112
+ socket.on('connect', () => {
113
+ const request = [
114
+ `GET ${path} HTTP/1.1`,
115
+ `Host: ${hostname}:${port}`,
116
+ 'Upgrade: websocket',
117
+ 'Connection: Upgrade',
118
+ `Sec-WebSocket-Key: ${randomBytes(16).toString('base64')}`,
119
+ 'Sec-WebSocket-Version: 13',
120
+ ...Object.entries(headers).map(([name, value]) => `${name}: ${value}`),
121
+ '',
122
+ ''
123
+ ].join('\r\n');
124
+ // one write, so the pipelined frames can't trail the handshake
125
+ socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), ...pipelined.map(encode_client_text)]));
126
+ });
127
+ socket.on('data', (chunk) => {
128
+ buffer = Buffer.concat([buffer, chunk]);
129
+ if (!upgraded) {
130
+ const end = buffer.indexOf('\r\n\r\n');
131
+ if (end === -1)
132
+ return;
133
+ const status_line = buffer.subarray(0, end).toString('utf-8').split('\r\n')[0];
134
+ if (!/^HTTP\/1\.1 101\b/u.test(status_line)) {
135
+ reject(new Error(`raw ws: upgrade refused: ${status_line}`));
136
+ socket.destroy();
137
+ return;
138
+ }
139
+ upgraded = true;
140
+ buffer = buffer.subarray(end + 4);
141
+ resolve(client);
142
+ }
143
+ const taken = take_frames(buffer);
144
+ buffer = taken.rest;
145
+ for (const frame of taken.frames) {
146
+ frames.push(frame);
147
+ // Deliberately no reply to a close frame — this client withholds it.
148
+ for (const waiter of waiters.slice()) {
149
+ if (waiter.predicate(frame)) {
150
+ waiters.splice(waiters.indexOf(waiter), 1);
151
+ waiter.resolve(frame);
152
+ }
153
+ }
154
+ }
155
+ });
156
+ });
@@ -44,9 +44,37 @@ export interface WsTransportOptions {
44
44
  * `messages` array; `wait_for` checks already-received messages first
45
45
  * before waiting for new arrivals.
46
46
  *
47
+ * An open socket is not yet an **admitted** connection: both spines answer
48
+ * the handshake first and admit the connection a credential re-check later,
49
+ * and deliver no server-initiated message to it until then. Requests sent
50
+ * meanwhile are queued and answered, so a suite that only round-trips can
51
+ * use this directly; one that waits for a push (a broadcast, a targeted
52
+ * notification) opens with `create_admitted_ws_transport` instead.
53
+ *
47
54
  * @throws Error if the upgrade fails (status, network) — the rejection
48
55
  * message carries the underlying error so the test surfaces the real
49
56
  * cause rather than hanging.
50
57
  */
51
58
  export declare const create_ws_transport: (options: WsTransportOptions) => Promise<WsClient>;
59
+ /** The request id `create_admitted_ws_transport` round-trips `heartbeat` under. */
60
+ export declare const WS_ADMISSION_PROBE_ID = "admitted";
61
+ /**
62
+ * Open a real-upgrade WS client and round-trip `heartbeat` on it, so the
63
+ * backend has **admitted** the connection by the time this resolves.
64
+ *
65
+ * Both spines answer the upgrade handshake before the connection is
66
+ * registered and admitted (the credential is re-read in between), and deliver
67
+ * no broadcast or targeted notification to a connection still pending
68
+ * admission. A request is answered only once the connection is admitted, so
69
+ * the round trip is the one signal a client has. Suites that open a socket and
70
+ * then wait for a server-initiated message, or that need connections
71
+ * registered in a known order, open through this.
72
+ *
73
+ * `heartbeat` is a protocol action, present on every WS endpoint. Its response
74
+ * frame (id `WS_ADMISSION_PROBE_ID`) stays in the client's `messages`.
75
+ *
76
+ * @throws Error if the upgrade fails, or the heartbeat is refused or times out
77
+ * — the socket is closed first
78
+ */
79
+ export declare const create_admitted_ws_transport: (options: WsTransportOptions) => Promise<WsClient>;
52
80
  //# sourceMappingURL=ws_transport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,gBAAgB,CAAC;AAGxB,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,GAAU,SAAS,kBAAkB,KAAG,OAAO,CAAC,QAAQ,CAmJvF,CAAC"}
1
+ {"version":3,"file":"ws_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,gBAAgB,CAAC;AAIxB,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,GAAU,SAAS,kBAAkB,KAAG,OAAO,CAAC,QAAQ,CAwJvF,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,kBAAkB,KACzB,OAAO,CAAC,QAAQ,CASlB,CAAC"}