@fuzdev/fuz_app 0.117.1 → 0.119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) 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 +125 -31
  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 +72 -44
  44. package/dist/auth/audit_emitter.d.ts.map +1 -1
  45. package/dist/auth/audit_emitter.js +60 -16
  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/bootstrap_account.d.ts +3 -2
  54. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  55. package/dist/auth/bootstrap_account.js +3 -3
  56. package/dist/auth/cleanup.d.ts +219 -30
  57. package/dist/auth/cleanup.d.ts.map +1 -1
  58. package/dist/auth/cleanup.js +260 -45
  59. package/dist/auth/deps.d.ts +29 -1
  60. package/dist/auth/deps.d.ts.map +1 -1
  61. package/dist/auth/migrations.d.ts.map +1 -1
  62. package/dist/auth/migrations.js +27 -0
  63. package/dist/auth/resolved_auth.d.ts +105 -0
  64. package/dist/auth/resolved_auth.d.ts.map +1 -0
  65. package/dist/auth/resolved_auth.js +124 -0
  66. package/dist/auth/role_grant_offer_ddl.d.ts +4 -2
  67. package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
  68. package/dist/auth/role_grant_offer_ddl.js +10 -3
  69. package/dist/auth/role_grant_offer_queries.d.ts +16 -7
  70. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  71. package/dist/auth/role_grant_offer_queries.js +33 -15
  72. package/dist/auth/role_grant_offer_schema.d.ts +9 -0
  73. package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
  74. package/dist/auth/session_middleware.d.ts +8 -1
  75. package/dist/auth/session_middleware.d.ts.map +1 -1
  76. package/dist/auth/session_middleware.js +9 -3
  77. package/dist/auth/session_queries.d.ts +31 -6
  78. package/dist/auth/session_queries.d.ts.map +1 -1
  79. package/dist/auth/session_queries.js +31 -6
  80. package/dist/auth/standard_rpc_actions.d.ts +9 -9
  81. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  82. package/dist/auth/standard_rpc_actions.js +3 -3
  83. package/dist/db/CLAUDE.md +7 -1
  84. package/dist/db/migrate.d.ts +8 -13
  85. package/dist/db/migrate.d.ts.map +1 -1
  86. package/dist/db/migrate.js +9 -38
  87. package/dist/db/status.d.ts +58 -7
  88. package/dist/db/status.d.ts.map +1 -1
  89. package/dist/db/status.js +205 -97
  90. package/dist/http/CLAUDE.md +7 -3
  91. package/dist/http/db_routes.d.ts +6 -2
  92. package/dist/http/db_routes.d.ts.map +1 -1
  93. package/dist/http/db_routes.js +2 -2
  94. package/dist/periodic.d.ts +80 -0
  95. package/dist/periodic.d.ts.map +1 -0
  96. package/dist/periodic.js +122 -0
  97. package/dist/realtime/sse_auth_guard.d.ts +79 -49
  98. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  99. package/dist/realtime/sse_auth_guard.js +103 -73
  100. package/dist/realtime/subscriber_registry.d.ts +119 -8
  101. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  102. package/dist/realtime/subscriber_registry.js +184 -25
  103. package/dist/runtime/deno.d.ts.map +1 -1
  104. package/dist/runtime/deno.js +9 -6
  105. package/dist/runtime/deps.d.ts +3 -2
  106. package/dist/runtime/deps.d.ts.map +1 -1
  107. package/dist/runtime/secure_file.d.ts +23 -9
  108. package/dist/runtime/secure_file.d.ts.map +1 -1
  109. package/dist/runtime/secure_file.js +29 -10
  110. package/dist/server/app_backend.d.ts +13 -2
  111. package/dist/server/app_backend.d.ts.map +1 -1
  112. package/dist/server/app_backend.js +5 -2
  113. package/dist/server/app_server.d.ts +55 -15
  114. package/dist/server/app_server.d.ts.map +1 -1
  115. package/dist/server/app_server.js +41 -13
  116. package/dist/testing/CLAUDE.md +140 -48
  117. package/dist/testing/app_server.d.ts +2 -2
  118. package/dist/testing/app_server.d.ts.map +1 -1
  119. package/dist/testing/app_server.js +11 -2
  120. package/dist/testing/audit_drift_guard.d.ts +3 -52
  121. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  122. package/dist/testing/audit_drift_guard.js +2 -46
  123. package/dist/testing/connection_closer_helpers.d.ts +9 -22
  124. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  125. package/dist/testing/connection_closer_helpers.js +11 -17
  126. package/dist/testing/cross_backend/capabilities.d.ts +67 -13
  127. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/capabilities.js +27 -12
  129. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  130. package/dist/testing/cross_backend/default_backend_configs.js +21 -0
  131. package/dist/testing/cross_backend/expected_schema.json +1 -0
  132. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  133. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
  134. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  135. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
  136. package/dist/testing/cross_backend/session_cap.d.ts +13 -0
  137. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/session_cap.js +55 -19
  139. package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
  140. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  141. package/dist/testing/cross_backend/sse_round_trip.js +93 -26
  142. package/dist/testing/cross_backend/testing_server_bun.js +4 -4
  143. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  144. package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
  145. package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
  146. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
  147. package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
  148. package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
  149. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  150. package/dist/testing/cross_backend/ws_round_trip.js +156 -19
  151. package/dist/testing/sse_round_trip.d.ts +6 -4
  152. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  153. package/dist/testing/stubs.d.ts +1 -1
  154. package/dist/testing/stubs.d.ts.map +1 -1
  155. package/dist/testing/stubs.js +6 -4
  156. package/dist/testing/transports/sse_transport.d.ts +1 -1
  157. package/dist/testing/transports/ws_client.d.ts +7 -0
  158. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  159. package/dist/testing/transports/ws_raw_client.d.ts +51 -0
  160. package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
  161. package/dist/testing/transports/ws_raw_client.js +156 -0
  162. package/dist/testing/transports/ws_transport.d.ts +28 -0
  163. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  164. package/dist/testing/transports/ws_transport.js +45 -1
  165. package/dist/testing/ws_round_trip.d.ts +8 -3
  166. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  167. package/dist/testing/ws_round_trip.js +19 -4
  168. package/dist/ui/AdminAccounts.svelte +4 -3
  169. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  170. package/dist/ui/AdminOverview.svelte +3 -1
  171. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  172. package/dist/ui/CLAUDE.md +22 -6
  173. package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
  174. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  175. package/dist/ui/admin_accounts_state.svelte.js +16 -0
  176. package/dist/ui/auth_state.svelte.d.ts +25 -1
  177. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  178. package/dist/ui/auth_state.svelte.js +28 -1
  179. package/package.json +2 -2
@@ -0,0 +1,131 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-backend parity suite for the **per-account WebSocket connection cap**
4
+ * over a real upgrade.
5
+ *
6
+ * Both spines bound an account's concurrent WebSocket connections
7
+ * (`BackendWebsocketTransport`'s `max_connections_per_account` on the TS side,
8
+ * the Rust `fuz_realtime` `ConnectionRegistry` on the other; both default to
9
+ * `DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT`) with the same policy: **evict-oldest**
10
+ * — a connection past the cap is always admitted, and the account's oldest is
11
+ * closed with `WS_CLOSE_CONNECTION_LIMIT` and the reason `connection limit`.
12
+ * A client reads that close as "superseded": closed until the user acts, not
13
+ * revoked and not reconnecting (`FrontendWebsocketClient.superseded`). The
14
+ * code, the reason, and which socket closes are therefore wire contract, and
15
+ * this suite is the pin that runs on both. Two cases:
16
+ *
17
+ * - **the oldest is superseded, and it is not a revocation** — on one session,
18
+ * opening one socket past the cap closes exactly the first with the
19
+ * connection-limit code and reason; every other socket, the newest included,
20
+ * stays open and still dispatches. The session then upgrades again, which a
21
+ * revoked session could not — and that upgrade supersedes the next-oldest
22
+ * in turn.
23
+ * - **a closed connection frees its slot** — one socket held open while a
24
+ * cap's worth of others open and close in sequence is never evicted. A
25
+ * backend that leaks closed connections in its registry counts them toward
26
+ * the cap and closes the held socket, which is what this catches.
27
+ *
28
+ * Sockets open in sequence, each answering a `heartbeat` before the next
29
+ * opens (`create_admitted_ws_transport`), so the backend registered them in
30
+ * that order and "oldest" names the same socket on both spines (an upgrade
31
+ * answers its handshake before the connection is registered and admitted, so
32
+ * the handshake alone doesn't order them).
33
+ *
34
+ * **Consumer-agnostic**, like `describe_cross_process_ws_tests`: it drives
35
+ * only the `heartbeat` protocol action, present on every WS endpoint. Gated on
36
+ * `capabilities.ws`; cross-process only (`create_ws_transport` needs a real
37
+ * bound socket). Each case opens more than a cap's worth of real sockets,
38
+ * which is why it's a separate call rather than more cases on the WS
39
+ * round-trip suite.
40
+ *
41
+ * `$lib`-free by contract (relative specifiers only), like the sibling
42
+ * cross-backend suites.
43
+ *
44
+ * @module
45
+ */
46
+ import { assert, describe } from 'vitest';
47
+ import { heartbeat_action_spec } from '../../actions/heartbeat.js';
48
+ import { WS_CLOSE_CONNECTION_LIMIT } from '../../actions/transports.js';
49
+ import { DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT } from '../../actions/transports_ws_backend.js';
50
+ import { create_admitted_ws_transport } from '../transports/ws_transport.js';
51
+ import { test_if } from './capabilities.js';
52
+ /** The close reason both spines send with `WS_CLOSE_CONNECTION_LIMIT`. */
53
+ const CONNECTION_LIMIT_REASON = 'connection limit';
54
+ /**
55
+ * Register the WebSocket connection-cap parity suite over a real upgrade:
56
+ * one connection past the cap closes the account's oldest with
57
+ * `WS_CLOSE_CONNECTION_LIMIT` and nothing else, and a closed connection
58
+ * frees its slot.
59
+ */
60
+ export const describe_ws_connection_cap_cross_tests = (options) => {
61
+ const { setup_test, capabilities, base_url, ws_path, origin } = options;
62
+ const max = options.max_connections_per_account === undefined
63
+ ? DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT
64
+ : options.max_connections_per_account;
65
+ const enabled = capabilities.ws && max !== null;
66
+ /**
67
+ * Open a socket the backend has admitted (a `heartbeat` round trip), so
68
+ * the connections are registered in the order they were opened.
69
+ */
70
+ const open_registered = (cookies) => create_admitted_ws_transport({ base_url, ws_path, cookies, origin });
71
+ const assert_superseded = async (client, label) => {
72
+ const closed = await client.wait_for_close(2000);
73
+ assert.ok(closed, `${label} did not close within 2s of the cap overflow`);
74
+ assert.strictEqual(client.close_code, WS_CLOSE_CONNECTION_LIMIT, `${label} close code`);
75
+ assert.strictEqual(client.close_reason, CONNECTION_LIMIT_REASON, `${label} close reason`);
76
+ };
77
+ /** Every client still open and answering — a fresh request id per call via `id`. */
78
+ const assert_all_answer = async (clients, id, message) => {
79
+ const results = await Promise.all(clients.map((client) => client.request(id, heartbeat_action_spec.method, {})));
80
+ assert.deepStrictEqual(results, clients.map(() => ({})), message);
81
+ assert.deepStrictEqual(clients.map((client) => client.close_code), clients.map(() => null), message);
82
+ };
83
+ // each case opens more than a cap's worth of real sockets in sequence, so its
84
+ // time scales with the cap and the backend's upgrade cost
85
+ describe('per-account websocket connection cap parity', { timeout: 30_000 }, () => {
86
+ test_if(enabled, 'one connection past the cap closes the oldest with WS_CLOSE_CONNECTION_LIMIT, and only it', async () => {
87
+ const fixture = await setup_test();
88
+ const cookies = fixture.transport.cookies();
89
+ const sockets = [];
90
+ try {
91
+ for (let i = 0; i < max; i++) {
92
+ sockets.push(await open_registered(cookies));
93
+ }
94
+ // at the cap, nothing has closed
95
+ await assert_all_answer(sockets, 'at_cap', 'every socket at the cap stays open');
96
+ // one past it: always admitted, never refused
97
+ sockets.push(await open_registered(cookies));
98
+ const [oldest, ...survivors] = sockets;
99
+ await assert_superseded(oldest, 'the oldest socket');
100
+ await assert_all_answer(survivors, 'past_cap', 'only the oldest may close; every other socket, the newest included, keeps answering');
101
+ // Not a revocation — the session still authenticates an upgrade
102
+ // (a revoked one is refused there). That upgrade is one more past
103
+ // the cap, so it supersedes the next-oldest.
104
+ sockets.push(await open_registered(cookies));
105
+ await assert_superseded(survivors[0], 'the next-oldest socket');
106
+ await assert_all_answer(sockets.slice(2), 'past_cap_again', 'the session keeps its newest sockets after a second overflow');
107
+ }
108
+ finally {
109
+ await Promise.all(sockets.map((client) => client.close().catch(() => { })));
110
+ }
111
+ });
112
+ test_if(enabled, 'a closed connection frees its slot', async () => {
113
+ const fixture = await setup_test();
114
+ const cookies = fixture.transport.cookies();
115
+ const held = await open_registered(cookies);
116
+ try {
117
+ // A cap's worth of connections, one at a time, each closed before
118
+ // the next opens — the account never holds more than a few at once,
119
+ // however late the backend notices each close.
120
+ for (let i = 0; i < max; i++) {
121
+ const client = await open_registered(cookies);
122
+ await client.close();
123
+ }
124
+ await assert_all_answer([held], 'held', 'closed connections must not count toward the cap — the held socket was evicted');
125
+ }
126
+ finally {
127
+ await held.close().catch(() => { });
128
+ }
129
+ });
130
+ });
131
+ };
@@ -37,7 +37,8 @@ export interface CrossProcessWsTestOptions {
37
37
  * `heartbeat` round-trip, `heartbeat`'s parameterless-shape contract, an
38
38
  * oversized message closing with `WS_CLOSE_MESSAGE_TOO_BIG`, anonymous-upgrade
39
39
  * refusal, disallowed-origin refusal, and — when `rpc_path` is supplied —
40
- * session-revocation closing the live socket.
40
+ * session revocation and account deletion closing the live socket, plus a
41
+ * self-revoking socket reading its reply before its close.
41
42
  */
42
43
  export declare const describe_cross_process_ws_tests: (options: CrossProcessWsTestOptions) => void;
43
44
  //# sourceMappingURL=ws_round_trip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,iEAAiE;AACjE,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,yBAAyB,KAAG,IAwIpF,CAAC"}
1
+ {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6F9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,iEAAiE;AACjE,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,yBAAyB,KAAG,IA+RpF,CAAC"}
@@ -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":"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"}
@@ -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"}