@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.
- 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 +125 -31
- 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 +72 -44
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +60 -16
- 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/bootstrap_account.d.ts +3 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +3 -3
- package/dist/auth/cleanup.d.ts +219 -30
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +260 -45
- package/dist/auth/deps.d.ts +29 -1
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +27 -0
- 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/role_grant_offer_ddl.d.ts +4 -2
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +10 -3
- package/dist/auth/role_grant_offer_queries.d.ts +16 -7
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +33 -15
- package/dist/auth/role_grant_offer_schema.d.ts +9 -0
- package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
- package/dist/auth/session_middleware.d.ts +8 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +9 -3
- package/dist/auth/session_queries.d.ts +31 -6
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +31 -6
- package/dist/auth/standard_rpc_actions.d.ts +9 -9
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +3 -3
- package/dist/db/CLAUDE.md +7 -1
- package/dist/db/migrate.d.ts +8 -13
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +9 -38
- package/dist/db/status.d.ts +58 -7
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -97
- 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/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +9 -6
- package/dist/runtime/deps.d.ts +3 -2
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/secure_file.d.ts +23 -9
- package/dist/runtime/secure_file.d.ts.map +1 -1
- package/dist/runtime/secure_file.js +29 -10
- package/dist/server/app_backend.d.ts +13 -2
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +5 -2
- package/dist/server/app_server.d.ts +55 -15
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +41 -13
- package/dist/testing/CLAUDE.md +140 -48
- package/dist/testing/app_server.d.ts +2 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -2
- package/dist/testing/audit_drift_guard.d.ts +3 -52
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +2 -46
- 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/expected_schema.json +1 -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
|
@@ -38,7 +38,7 @@ import '../assert_dev_env.js';
|
|
|
38
38
|
* guard), and accept stays idempotent on the role_grant while still superseding
|
|
39
39
|
* pending siblings. Gated on `capabilities.ws`.
|
|
40
40
|
*
|
|
41
|
-
* Cross-process only: `
|
|
41
|
+
* Cross-process only: `create_admitted_ws_transport` needs a real bound socket, so wire
|
|
42
42
|
* it from a `*.cross.test.ts` file, never an in-process setup. Authed cookies
|
|
43
43
|
* come from the per-account session minted by `fixture.create_account` /
|
|
44
44
|
* `fixture.create_session_headers`.
|
|
@@ -47,7 +47,7 @@ import '../assert_dev_env.js';
|
|
|
47
47
|
*/
|
|
48
48
|
import { assert, describe } from 'vitest';
|
|
49
49
|
import { rpc_call } from '../rpc_helpers.js';
|
|
50
|
-
import {
|
|
50
|
+
import { create_admitted_ws_transport } from '../transports/ws_transport.js';
|
|
51
51
|
import { is_notification_with } from '../transports/ws_client.js';
|
|
52
52
|
import { ROLE_ADMIN } from '../../auth/role_schema.js';
|
|
53
53
|
import { ROLE_GRANT_OFFER_RECEIVED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_ACCEPTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_DECLINED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_RETRACTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_SUPERSEDE_NOTIFICATION_METHOD, ROLE_GRANT_REVOKE_NOTIFICATION_METHOD, RoleGrantOfferReceivedParams, RoleGrantOfferAcceptedParams, RoleGrantOfferDeclinedParams, RoleGrantOfferRetractedParams, RoleGrantOfferSupersedeParams, RoleGrantRevokeParams } from '../../auth/role_grant_offer_notifications.js';
|
|
@@ -65,10 +65,19 @@ const RPC_PATH = '/api/rpc';
|
|
|
65
65
|
export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
66
66
|
const { setup_test, capabilities, base_url, ws_path } = options;
|
|
67
67
|
// -- shared helpers -------------------------------------------------------
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Open a WS transport for a single session cookie (`<name>=<value>`),
|
|
70
|
+
* admitted before it resolves: a targeted notification is delivered only to
|
|
71
|
+
* an admitted connection, and the handshake alone doesn't say it is one.
|
|
72
|
+
*/
|
|
69
73
|
const open_ws = (cookie) => {
|
|
70
74
|
assert.ok(cookie, 'expected a session cookie for the WS upgrade');
|
|
71
|
-
return
|
|
75
|
+
return create_admitted_ws_transport({
|
|
76
|
+
base_url,
|
|
77
|
+
ws_path,
|
|
78
|
+
cookies: [cookie],
|
|
79
|
+
origin: base_url
|
|
80
|
+
});
|
|
72
81
|
};
|
|
73
82
|
/** Drive a JSON-RPC call over the fixture's HTTP transport. */
|
|
74
83
|
const rpc = (fixture, method, params, headers) => rpc_call({ app: fixture.transport, path: RPC_PATH, method, params, headers });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0C9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,6CAA6C,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;
|
|
1
|
+
{"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0C9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,6CAA6C,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAqBnG,kGAAkG;AAClG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,sDACG,CAAC;AAErD,MAAM,WAAW,6BAA6B;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAkDF,CAAC"}
|
|
@@ -61,7 +61,9 @@ export const RUST_SPINE_STUB_EXPECTED_SCHEMA_PATH_ENV = 'FUZ_RUST_SPINE_STUB_EXP
|
|
|
61
61
|
* Capabilities for the Rust `testing_spine_stub` — `rust_default_capabilities`
|
|
62
62
|
* plus `sse` (the stub serves `GET /api/admin/audit/stream` over the spine
|
|
63
63
|
* `fuz_realtime::SseRegistry` + audit listener), `ready` (it live-mounts
|
|
64
|
-
* `/ready` over the env-supplied fixture path),
|
|
64
|
+
* `/ready` over the env-supplied fixture path), `ws_account_actions` (it
|
|
65
|
+
* serves one action registry on `/api/rpc` and `/api/ws`), and
|
|
66
|
+
* `cell_gated_create` (it
|
|
65
67
|
* mounts the `TestCellGatedCreateAuthorize` policy on its cell layer). Named
|
|
66
68
|
* (not inline) so every spine preset is greppable, mirroring
|
|
67
69
|
* `ts_spine_capabilities`.
|
|
@@ -70,6 +72,7 @@ const rust_spine_stub_capabilities = Object.freeze({
|
|
|
70
72
|
...rust_default_capabilities,
|
|
71
73
|
sse: true,
|
|
72
74
|
ready: true,
|
|
75
|
+
ws_account_actions: true,
|
|
73
76
|
cell_gated_create: true
|
|
74
77
|
});
|
|
75
78
|
/** Default listening port — slots beside zzz's 1175/1176; matches the binary's `DEFAULT_PORT`. */
|
|
@@ -14,6 +14,19 @@ export interface SessionCapCrossTestOptions {
|
|
|
14
14
|
readonly login_path?: string;
|
|
15
15
|
/** REST account-status route path. Default `/api/account/status`. */
|
|
16
16
|
readonly status_path?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Where the backend's WebSocket endpoint is, for the case that an evicted
|
|
19
|
+
* session's socket is closed. Pass it when the backend has a WS transport
|
|
20
|
+
* (`capabilities.ws`); omitted, that case is skipped.
|
|
21
|
+
*/
|
|
22
|
+
readonly ws?: {
|
|
23
|
+
/** Base URL the backend is reachable at (e.g. `http://localhost:1178`). */
|
|
24
|
+
readonly base_url: string;
|
|
25
|
+
/** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
|
|
26
|
+
readonly ws_path: string;
|
|
27
|
+
/** Origin for the upgrade. Defaults to `base_url`. */
|
|
28
|
+
readonly origin?: string;
|
|
29
|
+
};
|
|
17
30
|
}
|
|
18
31
|
export declare const describe_session_cap_cross_tests: (options: SessionCapCrossTestOptions) => void;
|
|
19
32
|
//# sourceMappingURL=session_cap.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_cap.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/session_cap.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"session_cap.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/session_cap.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6D9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,gDAAgD;AAChD,MAAM,WAAW,0BAA0B;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE;QACb,2EAA2E;QAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,+DAA+D;QAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,sDAAsD;QACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACF;AAED,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IA4FtF,CAAC"}
|
|
@@ -3,15 +3,11 @@ import '../assert_dev_env.js';
|
|
|
3
3
|
* Cross-backend parity suite for the **per-account concurrent-session cap**
|
|
4
4
|
* over real HTTP.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (`session_middleware.lifecycle.db.test.ts`, `session_token_limits.integration.db.test.ts`)
|
|
12
|
-
* whose Rust counterparts simply didn't exist to fail. This suite is the pin
|
|
13
|
-
* that runs on *both*, so the next spine to lose the cap fails here rather than
|
|
14
|
-
* in production. Two properties:
|
|
6
|
+
* Both spines cap concurrent sessions per account (the TS
|
|
7
|
+
* `query_session_enforce_limit` / `DEFAULT_MAX_SESSIONS`, the Rust
|
|
8
|
+
* `create_capped_session`). Each spine's own tests pin its half in-process;
|
|
9
|
+
* nothing in them crosses the wire, so a spine that lost the cap would fail
|
|
10
|
+
* no shared test. This suite is the pin that runs on *both*. It pins:
|
|
15
11
|
*
|
|
16
12
|
* - **the cap bounds concurrent sessions, by eviction not refusal** — logging in
|
|
17
13
|
* `max_sessions + 1` times succeeds every time (a login is never denied for
|
|
@@ -19,6 +15,11 @@ import '../assert_dev_env.js';
|
|
|
19
15
|
* - **the evicted session is the oldest** — the first login's cookie no longer
|
|
20
16
|
* authenticates once the cap is exceeded. A spine with no cap authenticates it
|
|
21
17
|
* fine, which is exactly the defect this catches.
|
|
18
|
+
* - **an evicted session's socket is closed** (when `ws` is supplied) — the cap
|
|
19
|
+
* deletes the session row, and a WebSocket never re-reads its session, so
|
|
20
|
+
* the login that evicts it must close the socket it opened
|
|
21
|
+
* (`WS_CLOSE_SESSION_REVOKED`), once the eviction has committed. A spine
|
|
22
|
+
* that only deletes the row leaves the socket running on a dead session.
|
|
22
23
|
*
|
|
23
24
|
* **Why `max_sessions + 1` logins and not `max_sessions`.** The starting session
|
|
24
25
|
* count isn't zero and isn't guaranteed equal across impls — `create_account`
|
|
@@ -48,16 +49,32 @@ import '../assert_dev_env.js';
|
|
|
48
49
|
* @module
|
|
49
50
|
*/
|
|
50
51
|
import { describe, test, assert } from 'vitest';
|
|
52
|
+
import { heartbeat_action_spec } from '../../actions/heartbeat.js';
|
|
53
|
+
import { WS_CLOSE_SESSION_REVOKED } from '../../actions/transports.js';
|
|
51
54
|
import { DEFAULT_MAX_SESSIONS } from '../../auth/account_route_schema.js';
|
|
52
55
|
import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
|
|
56
|
+
import { create_ws_transport } from '../transports/ws_transport.js';
|
|
57
|
+
import { test_if } from './capabilities.js';
|
|
53
58
|
export const describe_session_cap_cross_tests = (options) => {
|
|
54
|
-
const { setup_test } = options;
|
|
59
|
+
const { setup_test, ws } = options;
|
|
55
60
|
const max_sessions = options.max_sessions ?? DEFAULT_MAX_SESSIONS;
|
|
56
61
|
const login_path = options.login_path ?? '/api/account/login';
|
|
57
62
|
const status_path = options.status_path ?? '/api/account/status';
|
|
58
63
|
// Fresh-keeper-per-test wipes the DB between tests, so a literal username
|
|
59
64
|
// never collides (see `setup.ts`).
|
|
60
65
|
const username = 'session_cap_user';
|
|
66
|
+
/** Log `username` in on a fresh transport — one login, one session, one jar. */
|
|
67
|
+
const login = async (fixture, label) => {
|
|
68
|
+
const transport = fixture.fresh_transport();
|
|
69
|
+
const res = await transport(login_path, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'content-type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({ username, password: DEFAULT_TEST_PASSWORD })
|
|
73
|
+
});
|
|
74
|
+
assert.strictEqual(res.status, 200, `${label} must succeed — the cap evicts, it never refuses`);
|
|
75
|
+
assert.ok(transport.cookies().length > 0, `${label} must set a session cookie on its jar`);
|
|
76
|
+
return transport;
|
|
77
|
+
};
|
|
61
78
|
describe('per-account session cap parity', () => {
|
|
62
79
|
test(`logging in ${max_sessions + 1} times evicts the oldest session (newest cookie resolves, first does not)`, async () => {
|
|
63
80
|
const fixture = await setup_test();
|
|
@@ -69,15 +86,7 @@ export const describe_session_cap_cross_tests = (options) => {
|
|
|
69
86
|
// look alike. Held oldest-first.
|
|
70
87
|
const sessions = [];
|
|
71
88
|
for (let i = 0; i < max_sessions + 1; i++) {
|
|
72
|
-
|
|
73
|
-
const res = await transport(login_path, {
|
|
74
|
-
method: 'POST',
|
|
75
|
-
headers: { 'content-type': 'application/json' },
|
|
76
|
-
body: JSON.stringify({ username, password: DEFAULT_TEST_PASSWORD })
|
|
77
|
-
});
|
|
78
|
-
assert.strictEqual(res.status, 200, `login ${i + 1}/${max_sessions + 1} must succeed — the cap evicts, it never refuses`);
|
|
79
|
-
assert.ok(transport.cookies().length > 0, `login ${i + 1} must set a session cookie on its jar`);
|
|
80
|
-
sessions.push(transport);
|
|
89
|
+
sessions.push(await login(fixture, `login ${i + 1}/${max_sessions + 1}`));
|
|
81
90
|
}
|
|
82
91
|
/** `GET /status` on one login's transport: 200 authenticated, 401 not. */
|
|
83
92
|
const status_on = async (transport) => {
|
|
@@ -89,5 +98,32 @@ export const describe_session_cap_cross_tests = (options) => {
|
|
|
89
98
|
assert.strictEqual(await status_on(sessions[0]), 401, `the oldest session must be evicted once the account exceeds ${max_sessions} sessions ` +
|
|
90
99
|
'(a spine with no cap authenticates this cookie)');
|
|
91
100
|
});
|
|
101
|
+
// `max_sessions` further logins leave the watched session outside the
|
|
102
|
+
// newest `max_sessions`, so it is evicted whatever sessions the account
|
|
103
|
+
// held before it (`create_account` seeds one on some cradles).
|
|
104
|
+
test_if(ws !== undefined, 'a session the cap evicts has its open socket closed', async () => {
|
|
105
|
+
const fixture = await setup_test();
|
|
106
|
+
await fixture.create_account({ username, password_value: DEFAULT_TEST_PASSWORD });
|
|
107
|
+
const victim = await login(fixture, 'the login whose socket is watched');
|
|
108
|
+
const client = await create_ws_transport({
|
|
109
|
+
base_url: ws.base_url,
|
|
110
|
+
ws_path: ws.ws_path,
|
|
111
|
+
cookies: victim.cookies(),
|
|
112
|
+
origin: ws.origin
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
// admitted and dispatching before the evicting logins
|
|
116
|
+
await client.request(1, heartbeat_action_spec.method, {});
|
|
117
|
+
for (let i = 0; i < max_sessions; i++) {
|
|
118
|
+
await login(fixture, `evicting login ${i + 1}/${max_sessions}`);
|
|
119
|
+
}
|
|
120
|
+
const closed = await client.wait_for_close(2000);
|
|
121
|
+
assert.ok(closed, `the socket of a session evicted past ${max_sessions} sessions did not close within 2s`);
|
|
122
|
+
assert.strictEqual(client.close_code, WS_CLOSE_SESSION_REVOKED);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await client.close();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
92
128
|
});
|
|
93
129
|
};
|
|
@@ -27,11 +27,19 @@ export interface CrossProcessSseTestOptions {
|
|
|
27
27
|
readonly rpc_path?: string;
|
|
28
28
|
/** Origin for the stream request. Defaults to `base_url`. */
|
|
29
29
|
readonly origin?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The backend's per-session audit-stream cap, which the cap case opens one
|
|
32
|
+
* stream past. Defaults to `AUDIT_LOG_SSE_MAX_PER_SCOPE`, the cap both
|
|
33
|
+
* spines apply unless overridden; `null` (a backend with the cap disabled)
|
|
34
|
+
* skips the case.
|
|
35
|
+
*/
|
|
36
|
+
readonly max_per_scope?: number | null;
|
|
30
37
|
}
|
|
31
38
|
/**
|
|
32
|
-
* Register the cross-process SSE round-trip suite.
|
|
39
|
+
* Register the cross-process SSE round-trip suite. Its cases run over a
|
|
33
40
|
* real streaming `fetch`: connected-comment, audit data frame, account-wide
|
|
34
|
-
* close-on-revoke,
|
|
41
|
+
* close-on-revoke, session-scoped close-on-revoke, close on account delete,
|
|
42
|
+
* and the per-session cap.
|
|
35
43
|
*/
|
|
36
44
|
export declare const describe_cross_process_sse_tests: (options: CrossProcessSseTestOptions) => void;
|
|
37
45
|
//# sourceMappingURL=sse_round_trip.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8E9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,kEAAkE;AAClE,MAAM,WAAW,0BAA0B;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AA0BD;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IA2PtF,CAAC"}
|
|
@@ -11,7 +11,7 @@ import '../assert_dev_env.js';
|
|
|
11
11
|
* (`describe_standard_cross_process_tests`) omits SSE by design, so consumers
|
|
12
12
|
* call this alongside it (paralleling `describe_cross_process_ws_tests`).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Its cases follow the in-process SSE self-test against fuz_app's
|
|
15
15
|
* standard audit-log stream:
|
|
16
16
|
*
|
|
17
17
|
* 1. **connects** — the stream opens and emits the `: connected` comment.
|
|
@@ -23,20 +23,28 @@ import '../assert_dev_env.js';
|
|
|
23
23
|
* stream opens so `create_account`'s own audit events (invite / signup /
|
|
24
24
|
* login / token) don't land on it.
|
|
25
25
|
* 3. **close-on-revoke, account-wide** (gated on `rpc_path`) — the subscriber's
|
|
26
|
-
* *own* sessions are revoked (`account_session_revoke_all`),
|
|
27
|
-
*
|
|
28
|
-
* the
|
|
29
|
-
* via `SseTransport.wait_for_close`.
|
|
26
|
+
* *own* sessions are revoked (`account_session_revoke_all`), which closes
|
|
27
|
+
* every stream of the account once the revoke commits (the handler's
|
|
28
|
+
* close, repeated by the audit guard on the `session_revoke_all` row).
|
|
29
|
+
* Asserted via `SseTransport.wait_for_close`.
|
|
30
30
|
* 4. **close-on-revoke, session-scoped** (gated on `rpc_path`) — the
|
|
31
31
|
* subscriber's *own* single session is revoked (`account_session_revoke`),
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* which closes only the stream registered under that session's hash (the
|
|
33
|
+
* distinct close cases 2–3 don't reach).
|
|
34
|
+
* 5. **close on account delete** (gated on `rpc_path` and
|
|
35
|
+
* `capabilities.account_lifecycle`) — the keeper soft-deletes a second
|
|
36
|
+
* admin whose stream is open; the delete revokes its credentials and
|
|
37
|
+
* closes its stream.
|
|
38
|
+
* 6. **per-session cap** — one stream past `max_per_scope` on one session
|
|
39
|
+
* closes the oldest (evict-oldest, no final frame) and leaves every other
|
|
40
|
+
* stream open. Needs no RPC: the cap runs inside the subscribe itself.
|
|
34
41
|
*
|
|
35
|
-
* The close-on-revoke matrix is layered: cases 3–
|
|
36
|
-
* and session-scoped paths cross-process; the remaining
|
|
37
|
-
* (`token_revoke_all` / `logout` / `password_change`, all
|
|
38
|
-
* `
|
|
39
|
-
*
|
|
42
|
+
* The close-on-revoke matrix is layered: cases 3–5 exercise the account-wide
|
|
43
|
+
* and session-scoped paths cross-process; the remaining revocation events
|
|
44
|
+
* (`token_revoke_all` / `logout` / `password_change` / `account_purge`, all
|
|
45
|
+
* account-wide; `token_revoke`, token-scoped; and `role_grant_revoke`,
|
|
46
|
+
* role-matched) are covered by the spine's `fuz_realtime` SSE-registry unit
|
|
47
|
+
* tests and the in-process guard self-test, so a cross-process
|
|
40
48
|
* `token_revoke_all`-with-zero-tokens case (which may emit no audit row) stays
|
|
41
49
|
* out to keep the spawned-backend suite non-flaky.
|
|
42
50
|
*
|
|
@@ -49,8 +57,9 @@ import '../assert_dev_env.js';
|
|
|
49
57
|
*/
|
|
50
58
|
import { assert, describe } from 'vitest';
|
|
51
59
|
import { account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec } from '../../auth/account_action_specs.js';
|
|
52
|
-
import { admin_session_revoke_all_action_spec } from '../../auth/admin_action_specs.js';
|
|
53
|
-
import {
|
|
60
|
+
import { account_delete_action_spec, admin_session_revoke_all_action_spec } from '../../auth/admin_action_specs.js';
|
|
61
|
+
import { ROLE_ADMIN } from '../../auth/role_schema.js';
|
|
62
|
+
import { AUDIT_LOG_SSE_MAX_PER_SCOPE, audit_log_event_specs } from '../../realtime/sse_auth_guard.js';
|
|
54
63
|
import { SSE_CONNECTED_COMMENT } from '../../realtime/sse_constants.js';
|
|
55
64
|
import { create_sse_transport } from '../transports/sse_transport.js';
|
|
56
65
|
import { create_rpc_post_init } from '../rpc_helpers.js';
|
|
@@ -73,13 +82,15 @@ const assert_audit_data_frame = (frame) => {
|
|
|
73
82
|
assert.ok(result.success, `audit data frame params mismatch for '${String(payload.method)}': ${result.success ? '' : JSON.stringify(result.error.issues)}`);
|
|
74
83
|
};
|
|
75
84
|
/**
|
|
76
|
-
* Register the cross-process SSE round-trip suite.
|
|
85
|
+
* Register the cross-process SSE round-trip suite. Its cases run over a
|
|
77
86
|
* real streaming `fetch`: connected-comment, audit data frame, account-wide
|
|
78
|
-
* close-on-revoke,
|
|
87
|
+
* close-on-revoke, session-scoped close-on-revoke, close on account delete,
|
|
88
|
+
* and the per-session cap.
|
|
79
89
|
*/
|
|
80
90
|
export const describe_cross_process_sse_tests = (options) => {
|
|
81
91
|
const { setup_test, capabilities, base_url, rpc_path, origin } = options;
|
|
82
92
|
const sse_path = options.sse_path ?? DEFAULT_SSE_PATH;
|
|
93
|
+
const max_per_scope = options.max_per_scope === undefined ? AUDIT_LOG_SSE_MAX_PER_SCOPE : options.max_per_scope;
|
|
83
94
|
describe('cross-process sse', () => {
|
|
84
95
|
test_if(capabilities.sse, 'connects and emits the connected comment', async () => {
|
|
85
96
|
const fixture = await setup_test();
|
|
@@ -127,8 +138,9 @@ export const describe_cross_process_sse_tests = (options) => {
|
|
|
127
138
|
await sse.close();
|
|
128
139
|
}
|
|
129
140
|
});
|
|
130
|
-
// Revoke the subscriber's OWN sessions →
|
|
131
|
-
//
|
|
141
|
+
// Revoke the subscriber's OWN sessions → the handler closes the keeper's
|
|
142
|
+
// streams once the revoke commits (the audit guard repeats the close on
|
|
143
|
+
// the `session_revoke_all` row).
|
|
132
144
|
test_if(capabilities.sse && rpc_path !== undefined, 'stream closes when the subscriber sessions are revoked', async () => {
|
|
133
145
|
const fixture = await setup_test();
|
|
134
146
|
const sse = await create_sse_transport({
|
|
@@ -150,14 +162,15 @@ export const describe_cross_process_sse_tests = (options) => {
|
|
|
150
162
|
}
|
|
151
163
|
});
|
|
152
164
|
// Single `session_revoke` of the subscriber's OWN session → the
|
|
153
|
-
// session-hash-scoped close
|
|
154
|
-
// `close_by_identity(
|
|
155
|
-
// so revoking it by its blake3 hash drops the stream
|
|
156
|
-
// This is the close-on-revoke path the account-wide
|
|
157
|
-
// exercise; the remaining
|
|
158
|
-
// `
|
|
159
|
-
// `session_revoke_all`
|
|
160
|
-
//
|
|
165
|
+
// session-hash-scoped close (the Rust registry's `close_for_session`, the
|
|
166
|
+
// TS registry's `close_by_identity(session_hash)`). The keeper holds
|
|
167
|
+
// exactly one session, so revoking it by its blake3 hash drops the stream
|
|
168
|
+
// opened under it. This is the close-on-revoke path the account-wide case
|
|
169
|
+
// above doesn't exercise; the remaining account-wide events
|
|
170
|
+
// (`token_revoke_all` / `logout` / `password_change` / `account_purge`)
|
|
171
|
+
// share the close the `session_revoke_all` and account-delete cases cover,
|
|
172
|
+
// and `token_revoke`'s token-scoped and `role_grant_revoke`'s role-matched
|
|
173
|
+
// closes are covered by `fuz_realtime`'s SSE registry unit tests.
|
|
161
174
|
test_if(capabilities.sse && rpc_path !== undefined, 'stream closes on a single session_revoke of the subscriber session', async () => {
|
|
162
175
|
const fixture = await setup_test();
|
|
163
176
|
const sse = await create_sse_transport({
|
|
@@ -183,5 +196,59 @@ export const describe_cross_process_sse_tests = (options) => {
|
|
|
183
196
|
await sse.close();
|
|
184
197
|
}
|
|
185
198
|
});
|
|
199
|
+
// A soft-deleted account's credentials stop authenticating, and a stream
|
|
200
|
+
// never rechecks its own — so the delete must close it. The keeper
|
|
201
|
+
// deletes a second admin whose stream is open; the keeper's own admin
|
|
202
|
+
// grant keeps the delete clear of the last-admin guard.
|
|
203
|
+
test_if(capabilities.sse && capabilities.account_lifecycle && rpc_path !== undefined, 'stream closes when the subscriber account is deleted', async () => {
|
|
204
|
+
const fixture = await setup_test();
|
|
205
|
+
const target = await fixture.create_account({
|
|
206
|
+
username: 'sse_delete_target',
|
|
207
|
+
roles: [ROLE_ADMIN]
|
|
208
|
+
});
|
|
209
|
+
const cookie = target.create_session_headers().cookie;
|
|
210
|
+
assert.ok(cookie, 'expected a session cookie for the target account');
|
|
211
|
+
const sse = await create_sse_transport({ base_url, sse_path, cookies: [cookie], origin });
|
|
212
|
+
try {
|
|
213
|
+
const first = await sse.read_frame();
|
|
214
|
+
assert.strictEqual(first + '\n\n', SSE_CONNECTED_COMMENT, 'first frame must be the connected comment');
|
|
215
|
+
const res = await fixture.transport(rpc_path, create_rpc_post_init(account_delete_action_spec.method, {
|
|
216
|
+
account_id: target.account.id
|
|
217
|
+
}));
|
|
218
|
+
assert.strictEqual(res.status, 200, `account_delete RPC failed (status=${res.status})`);
|
|
219
|
+
const closed = await sse.wait_for_close(2000);
|
|
220
|
+
assert.ok(closed, 'stream did not close within 2s after account_delete');
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
await sse.close();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// One stream past the cap on one session → the oldest closes, the newest
|
|
227
|
+
// stays open. Opened in sequence, each awaited to its connect comment, so
|
|
228
|
+
// the server registered them in order and "oldest" is stream 0 on both
|
|
229
|
+
// spines. The evicted stream ends with no final frame, so a browser
|
|
230
|
+
// `EventSource` would reconnect — this asserts the server side only.
|
|
231
|
+
test_if(capabilities.sse && max_per_scope !== null, 'one stream past the per-session cap closes the oldest', async () => {
|
|
232
|
+
const fixture = await setup_test();
|
|
233
|
+
const cookies = fixture.transport.cookies();
|
|
234
|
+
const streams = [];
|
|
235
|
+
try {
|
|
236
|
+
for (let i = 0; i <= max_per_scope; i++) {
|
|
237
|
+
const sse = await create_sse_transport({ base_url, sse_path, cookies, origin });
|
|
238
|
+
streams.push(sse);
|
|
239
|
+
const first = await sse.read_frame();
|
|
240
|
+
assert.strictEqual(first + '\n\n', SSE_CONNECTED_COMMENT, `stream ${i}: first frame must be the connected comment`);
|
|
241
|
+
}
|
|
242
|
+
const [oldest, ...survivors] = streams;
|
|
243
|
+
const closed = await oldest.wait_for_close(2000);
|
|
244
|
+
assert.ok(closed, 'the oldest stream did not close within 2s of the cap overflow');
|
|
245
|
+
// only the oldest — every later stream on the session stays open
|
|
246
|
+
const survivors_closed = await Promise.all(survivors.map((sse) => sse.wait_for_close(200)));
|
|
247
|
+
assert.deepStrictEqual(survivors_closed, survivors.map(() => false), 'only the oldest stream may close; the rest of the session stays open');
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await Promise.all(streams.map((sse) => sse.close()));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
186
253
|
});
|
|
187
254
|
};
|
|
@@ -53,10 +53,10 @@ export const create_bun_testing_adapter = () => ({
|
|
|
53
53
|
// hang), and the client runtime — a single server-closed WS is
|
|
54
54
|
// necessary and sufficient. Client-initiated close or leaving the
|
|
55
55
|
// socket open both stop cleanly in ~0ms. In this suite the trigger is
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
56
|
+
// any server-initiated WS close — a revocation's (the `ws.cross.test.ts`
|
|
57
|
+
// close-on-revoke cases), a cap eviction's, an oversized message's —
|
|
58
|
+
// which is why teardown hangs after those and not under HTTP-only or
|
|
59
|
+
// client-closed WS traffic.
|
|
60
60
|
//
|
|
61
61
|
// So initiate a force-close (`true` drops active connections, no
|
|
62
62
|
// drain) but DON'T await it: awaiting hangs `start_testing_server`'s
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AA0C3D,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAC3C;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgCF,CAAC"}
|
|
@@ -9,6 +9,7 @@ export const TS_SPINE_DIR_ENV = 'FUZ_TESTING_TS_SPINE_DIR';
|
|
|
9
9
|
* (the binary wires `audit_log_sse`), `ready` (the binary live-mounts the
|
|
10
10
|
* `/ready` deploy gate in `build_spine_app`), `peer_request` (the backend
|
|
11
11
|
* WS transport's `request_connection` path drives server→client `peer/ping`),
|
|
12
|
+
* `ws_account_actions` (the binary mounts the account actions on `/api/ws`),
|
|
12
13
|
* and `cell_gated_create` (the binary's `full_spine_mount` mounts the
|
|
13
14
|
* `test_cell_gated_create_authorize` policy).
|
|
14
15
|
*/
|
|
@@ -17,11 +18,13 @@ const ts_spine_capabilities = Object.freeze({
|
|
|
17
18
|
sse: true,
|
|
18
19
|
ready: true,
|
|
19
20
|
peer_request: true,
|
|
21
|
+
ws_account_actions: true,
|
|
20
22
|
cell_gated_create: true
|
|
21
23
|
});
|
|
22
24
|
/**
|
|
23
25
|
* Capabilities for the **Bun** spine binary — `ts_spine_capabilities` with
|
|
24
|
-
* `oversized_reject_closes_connection: false
|
|
26
|
+
* `oversized_reject_closes_connection: false` and
|
|
27
|
+
* `ws_handshake_pipelining: false`. `Bun.serve` drains the declared
|
|
25
28
|
* `Content-Length` of an oversized-body `413` reject and keeps the socket
|
|
26
29
|
* alive (processing the correctly-framed pipelined request) even when the
|
|
27
30
|
* response carries `Connection: close`, unlike `@hono/node-server` / Deno /
|
|
@@ -29,10 +32,15 @@ const ts_spine_capabilities = Object.freeze({
|
|
|
29
32
|
* there is no desync — but the smuggling suite's strong "connection closes"
|
|
30
33
|
* assertion doesn't hold; this flag routes Bun onto the suite's no-desync arm.
|
|
31
34
|
* See `docs/security.md` §"Body Size Limiting".
|
|
35
|
+
*
|
|
36
|
+
* `Bun.serve` also answers `400` to an upgrade request that carries WebSocket
|
|
37
|
+
* frames in the same TCP write, before any handler runs, so the WS suite's
|
|
38
|
+
* frames-with-the-handshake case has nothing to drive there.
|
|
32
39
|
*/
|
|
33
40
|
const ts_spine_bun_capabilities = Object.freeze({
|
|
34
41
|
...ts_spine_capabilities,
|
|
35
|
-
oversized_reject_closes_connection: false
|
|
42
|
+
oversized_reject_closes_connection: false,
|
|
43
|
+
ws_handshake_pipelining: false
|
|
36
44
|
});
|
|
37
45
|
/** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
|
|
38
46
|
export const TS_SPINE_NODE_DEFAULT_PORT = 1178;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import { type BackendCapabilities } from './capabilities.js';
|
|
3
|
+
import type { SetupTest } from './setup.js';
|
|
4
|
+
/** Options for the WebSocket connection-cap parity suite. */
|
|
5
|
+
export interface WsConnectionCapCrossTestOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Per-test fixture producer (`default_cross_process_setup(handle)`). Every
|
|
8
|
+
* socket in a case upgrades with the fresh-per-test keeper's session
|
|
9
|
+
* cookies, so they all land on one account.
|
|
10
|
+
*/
|
|
11
|
+
readonly setup_test: SetupTest;
|
|
12
|
+
/** Backend capability flags; every case gates on `capabilities.ws`. */
|
|
13
|
+
readonly capabilities: BackendCapabilities;
|
|
14
|
+
/** Base URL the backend is reachable at (e.g. `http://localhost:1178`). */
|
|
15
|
+
readonly base_url: string;
|
|
16
|
+
/** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
|
|
17
|
+
readonly ws_path: string;
|
|
18
|
+
/** Origin for the upgrades. Defaults to `base_url`. */
|
|
19
|
+
readonly origin?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The backend's per-account connection cap, which each case opens past.
|
|
22
|
+
* Defaults to `DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT`, the cap both spines
|
|
23
|
+
* apply unless overridden; pass the consumer's value when it sets one, or
|
|
24
|
+
* `null` (a backend with the cap disabled) to skip the cases.
|
|
25
|
+
*/
|
|
26
|
+
readonly max_connections_per_account?: number | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register the WebSocket connection-cap parity suite over a real upgrade:
|
|
30
|
+
* one connection past the cap closes the account's oldest with
|
|
31
|
+
* `WS_CLOSE_CONNECTION_LIMIT` and nothing else, and a closed connection
|
|
32
|
+
* frees its slot.
|
|
33
|
+
*/
|
|
34
|
+
export declare const describe_ws_connection_cap_cross_tests: (options: WsConnectionCapCrossTestOptions) => void;
|
|
35
|
+
//# sourceMappingURL=ws_connection_cap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws_connection_cap.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_connection_cap.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsD9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,6DAA6D;AAC7D,MAAM,WAAW,+BAA+B;IAC/C;;;;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,uDAAuD;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,2BAA2B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,+BAA+B,KACtC,IA4GF,CAAC"}
|