@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.
- package/dist/actions/CLAUDE.md +301 -73
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +16 -1
- package/dist/actions/connection_closer.d.ts +136 -51
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +133 -34
- package/dist/actions/register_action_ws.d.ts +176 -24
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +581 -241
- package/dist/actions/register_ws_endpoint.d.ts +1 -0
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +1 -0
- package/dist/actions/socket.svelte.d.ts +43 -9
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +59 -8
- package/dist/actions/transports.d.ts +55 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +55 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +87 -111
- package/dist/actions/transports_ws_backend.d.ts +212 -30
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +327 -70
- package/dist/actions/ws_endpoint_spec.d.ts +26 -13
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +113 -32
- package/dist/auth/account_actions.d.ts +12 -18
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +47 -33
- package/dist/auth/account_routes.d.ts +5 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +40 -36
- package/dist/auth/account_schema.d.ts +27 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +22 -0
- package/dist/auth/admin_actions.d.ts +8 -17
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +33 -21
- package/dist/auth/api_token_queries.d.ts +23 -2
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +39 -3
- package/dist/auth/audit_emitter.d.ts +61 -38
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +57 -14
- package/dist/auth/audit_log_route_schema.js +7 -7
- package/dist/auth/audit_log_routes.d.ts +31 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +85 -11
- package/dist/auth/audit_log_schema.d.ts +48 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +58 -0
- package/dist/auth/cleanup.d.ts +203 -28
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +222 -26
- package/dist/auth/deps.d.ts +31 -3
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/resolved_auth.d.ts +105 -0
- package/dist/auth/resolved_auth.d.ts.map +1 -0
- package/dist/auth/resolved_auth.js +124 -0
- package/dist/auth/session_middleware.d.ts +8 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +9 -3
- package/dist/auth/session_queries.d.ts +31 -6
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +31 -6
- package/dist/auth/standard_rpc_actions.d.ts +9 -9
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +3 -3
- package/dist/db/CLAUDE.md +19 -8
- package/dist/db/fact_disk_storage.d.ts +10 -8
- package/dist/db/fact_disk_storage.d.ts.map +1 -1
- package/dist/db/fact_disk_storage.js +13 -17
- package/dist/db/fact_store.d.ts +39 -38
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/fact_store.js +48 -86
- package/dist/db/file_fact_url.d.ts +6 -6
- package/dist/db/file_fact_url.js +6 -6
- package/dist/db/status.d.ts +58 -1
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -77
- package/dist/http/CLAUDE.md +7 -3
- package/dist/http/db_routes.d.ts +6 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +2 -2
- package/dist/periodic.d.ts +80 -0
- package/dist/periodic.d.ts.map +1 -0
- package/dist/periodic.js +122 -0
- package/dist/realtime/sse_auth_guard.d.ts +79 -49
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +103 -73
- package/dist/realtime/subscriber_registry.d.ts +119 -8
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +184 -25
- package/dist/server/app_backend.d.ts +12 -1
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +5 -2
- package/dist/server/app_server.d.ts +55 -15
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +41 -13
- package/dist/server/serve_fact_route.d.ts +10 -7
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +11 -8
- package/dist/testing/CLAUDE.md +140 -48
- package/dist/testing/app_server.d.ts +2 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -2
- package/dist/testing/audit_drift_guard.d.ts +1 -48
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +0 -42
- package/dist/testing/connection_closer_helpers.d.ts +9 -22
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +11 -17
- package/dist/testing/cross_backend/capabilities.d.ts +67 -13
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +27 -12
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +21 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
- package/dist/testing/cross_backend/session_cap.d.ts +13 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +55 -19
- package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +93 -26
- package/dist/testing/cross_backend/testing_server_bun.js +4 -4
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +156 -19
- package/dist/testing/sse_round_trip.d.ts +6 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/sse_transport.d.ts +1 -1
- package/dist/testing/transports/ws_client.d.ts +7 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_raw_client.d.ts +51 -0
- package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_raw_client.js +156 -0
- package/dist/testing/transports/ws_transport.d.ts +28 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +45 -1
- package/dist/testing/ws_round_trip.d.ts +8 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -4
- package/dist/ui/AdminAccounts.svelte +4 -3
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +3 -1
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +22 -6
- package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -0
- package/dist/ui/auth_state.svelte.d.ts +25 -1
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +28 -1
- package/package.json +2 -2
- package/dist/server/file_fact_fetcher.d.ts +0 -42
- package/dist/server/file_fact_fetcher.d.ts.map +0 -1
- 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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
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
|
|
150
|
-
//
|
|
151
|
-
//
|
|
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
|
|
31
|
-
*
|
|
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`
|
|
49
|
-
*
|
|
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
|
|
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"}
|
package/dist/testing/stubs.d.ts
CHANGED
|
@@ -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
|
-
* `
|
|
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":"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;
|
|
1
|
+
{"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"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"}
|
package/dist/testing/stubs.js
CHANGED
|
@@ -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
|
-
* `
|
|
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
|
-
*
|
|
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":"AAAA,OAAO,sBAAsB,CAAC;AA8B9B,OAAO,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAa7E,MAAM,WAAW,wBAAwB,CAAC,CAAC,GAAG,OAAO;IACpD,OAAO,EAAE,OAAO,eAAe,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,CAAC,CAAC;CACV;AAED,MAAM,WAAW,2BAA2B,CAAC,CAAC,GAAG,OAAO;IACvD,OAAO,EAAE,OAAO,eAAe,CAAC;IAChC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,CAAC;CACV;AAED,MAAM,WAAW,yBAAyB,CAAC,CAAC,GAAG,OAAO;IACrD,OAAO,EAAE,OAAO,eAAe,CAAC;IAChC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,CAAA;KAAE,CAAC;CACnD;AAQD,6EAA6E;AAC7E,eAAO,MAAM,eAAe,GAC1B,QAAQ,MAAM,MACd,KAAK,OAAO,KAAG,OACsC,CAAC;AAExD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAC/B,CAAC,EAAE,QAAQ,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,MAChD,KAAK,OAAO,KAAG,GAAG,IAAI,wBAAwB,CAAC,CAAC,CAGE,CAAC;AAErD,gGAAgG;AAChG,eAAO,MAAM,eAAe,GAC1B,IAAI,MAAM,GAAG,MAAM,MACnB,KAAK,OAAO,KAAG,OAC8D,CAAC;AAQhF,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACxB,SAAS,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACrC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAC7B;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,yBAAyB,CAAC,OAAO,CAAC,CAAA;CAAE,GAAG,SAAS,CAAC;AAEjF;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAChC,OAAO,EAAE,cAAc,KACnB,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAC3B,QAAQ,OAAO,EACf,UAAU,KAAK,CAAC,OAAO,CAAC,EACxB,SAAS,KAAK,CAAC,QAAQ,CAAC,EACxB,YAAY,kBAAkB,GAAG,SAAS,EAC1C,OAAO,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,KAC/B,IAaF,CAAC;AA8BF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0EAA0E;AAC1E,MAAM,WAAW,QAAQ;IACxB;;;;;;OAMG;IACH,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C;;;;;;;;;;;OAWG;IACH,OAAO,EAAE,CAAC,CAAC,GAAG,OAAO,EACpB,EAAE,EAAE,MAAM,GAAG,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,EACf,UAAU,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB;;;;;OAKG;IACH,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD;;;;;;;;;;OAUG;IACH,cAAc,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAC1C;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE;QACT,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,GAAG,IAAI,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QAE5E,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;KACrF,CAAC;CACF"}
|
|
@@ -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;
|
|
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"}
|