@fuzdev/fuz_app 0.147.0 → 0.148.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 +6 -4
- package/dist/actions/action_codegen.d.ts +15 -1
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +23 -2
- package/dist/actions/perform_action.js +1 -1
- package/dist/auth/cell_action_specs.d.ts +3 -1
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +5 -5
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +2 -3
- package/dist/auth/cell_data_schema.d.ts +1 -1
- package/dist/auth/cell_data_schema.js +1 -1
- package/dist/auth/cell_field_action_specs.d.ts +4 -3
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +5 -6
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +8 -1
- package/dist/auth/cell_item_action_specs.d.ts +4 -2
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +5 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +8 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts +2 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +3 -1
- package/dist/primitive_schemas.d.ts +1 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +2 -1
- package/dist/schema_meta.d.ts +20 -0
- package/dist/schema_meta.d.ts.map +1 -1
- package/dist/schema_meta.js +7 -1
- package/dist/testing/CLAUDE.md +720 -114
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +73 -5
- package/dist/testing/adversarial_input.d.ts +0 -47
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +4 -340
- package/dist/testing/adversarial_input_cases.d.ts +64 -0
- package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
- package/dist/testing/adversarial_input_cases.js +348 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +24 -20
- package/dist/testing/audit_drift_guard.d.ts +27 -1
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +50 -0
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +29 -12
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/actor_lookup.js +7 -25
- package/dist/testing/cross_backend/app_settings.d.ts.map +1 -1
- package/dist/testing/cross_backend/app_settings.js +4 -6
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts +51 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/audit_cross_helpers.js +78 -0
- package/dist/testing/cross_backend/backend_config.d.ts +2 -1
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +6 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +26 -44
- package/dist/testing/cross_backend/body_size_smuggling.d.ts +5 -0
- package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.js +7 -14
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +3 -1
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +55 -59
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +20 -23
- package/dist/testing/cross_backend/cargo_binary.d.ts +70 -0
- package/dist/testing/cross_backend/cargo_binary.d.ts.map +1 -0
- package/dist/testing/cross_backend/cargo_binary.js +148 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +57 -1
- package/dist/testing/cross_backend/cell_gated_create.d.ts +0 -60
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +9 -487
- package/dist/testing/cross_backend/cell_grant_role.d.ts +14 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +22 -12
- package/dist/testing/cross_backend/cell_moderate.d.ts +66 -0
- package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_moderate.js +511 -0
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_ref_admission.js +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +40 -19
- package/dist/testing/cross_backend/conformance_case.d.ts +43 -4
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +29 -1
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts +93 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.js +125 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +144 -4
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +297 -65
- package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -1
- package/dist/testing/cross_backend/cookie_attributes.js +3 -2
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +38 -5
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/create_cross_backend_global_setup.js +24 -3
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +5 -5
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.d.ts +10 -6
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.js +32 -31
- package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_header_robustness.js +4 -6
- package/dist/testing/cross_backend/{cell_cross_helpers.d.ts → cross_rpc.d.ts} +12 -2
- package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -0
- package/dist/testing/cross_backend/{cell_cross_helpers.js → cross_rpc.js} +31 -7
- package/dist/testing/cross_backend/default_backend_configs.d.ts +56 -61
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +97 -91
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +2 -2
- package/dist/testing/cross_backend/differential.d.ts +126 -0
- package/dist/testing/cross_backend/differential.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential.js +201 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts +150 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential_normalize.js +153 -0
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +27 -17
- package/dist/testing/cross_backend/full_spine_mount.d.ts +15 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +55 -1
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/identity_parity.js +6 -5
- package/dist/testing/cross_backend/in_process_setup.d.ts +12 -2
- package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/in_process_setup.js +10 -7
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +13 -18
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/origin_conformance.js +1 -1
- package/dist/testing/cross_backend/origin_scope.d.ts +6 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin_scope.js +13 -12
- package/dist/testing/cross_backend/peer_ping_ws.d.ts +2 -5
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +14 -24
- package/dist/testing/cross_backend/postgres_database.d.ts +85 -0
- package/dist/testing/cross_backend/postgres_database.d.ts.map +1 -0
- package/dist/testing/cross_backend/postgres_database.js +140 -0
- package/dist/testing/cross_backend/provided_context.d.ts +28 -0
- package/dist/testing/cross_backend/provided_context.d.ts.map +1 -0
- package/dist/testing/cross_backend/provided_context.js +35 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +10 -12
- package/dist/testing/cross_backend/ready.js +2 -2
- package/dist/testing/cross_backend/revocation_drivers.d.ts +4 -3
- package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -1
- package/dist/testing/cross_backend/revocation_drivers.js +1 -1
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +3 -2
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +203 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +2 -5
- 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 +203 -111
- package/dist/testing/cross_backend/role_grant_participation.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +32 -21
- package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_role_grammar.js +6 -9
- package/dist/testing/cross_backend/rpc_adversarial.d.ts +38 -0
- package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_adversarial.js +292 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts +24 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.js +124 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +101 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.js +113 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +10 -0
- 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 +27 -18
- package/dist/testing/cross_backend/session_cap.d.ts +9 -8
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +9 -4
- package/dist/testing/cross_backend/session_cookie_codec.js +1 -1
- package/dist/testing/cross_backend/setup.d.ts +56 -54
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +29 -29
- package/dist/testing/cross_backend/simple_request.d.ts.map +1 -1
- package/dist/testing/cross_backend/simple_request.js +4 -4
- package/dist/testing/cross_backend/spawn_backend.d.ts +59 -10
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
- package/dist/testing/cross_backend/spawn_backend.js +219 -60
- package/dist/testing/cross_backend/spine_suites.d.ts +231 -0
- package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_suites.js +626 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +29 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +29 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +3 -4
- package/dist/testing/cross_backend/standard.d.ts +18 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +2 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +24 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +15 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts +2 -8
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +7 -10
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +3 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +8 -4
- package/dist/testing/cross_backend/testing_server_bun.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +1 -1
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +9 -13
- package/dist/testing/cross_backend/transport_composition.d.ts +26 -2
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -1
- package/dist/testing/cross_backend/transport_composition.js +81 -51
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +8 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +17 -10
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +2 -5
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +8 -14
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +4 -9
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_connection_cap.js +11 -12
- package/dist/testing/cross_backend/ws_endpoint.d.ts +52 -0
- package/dist/testing/cross_backend/ws_endpoint.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_endpoint.js +54 -0
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +2 -5
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_inbound_dispatch.js +9 -15
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +2 -7
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_outbound_overflow.js +5 -9
- package/dist/testing/cross_backend/ws_role_gated.d.ts +4 -5
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_role_gated.js +12 -21
- package/dist/testing/cross_backend/ws_round_trip.d.ts +12 -9
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +29 -34
- package/dist/testing/db_entities.d.ts +16 -3
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +28 -5
- package/dist/testing/integration_helpers.d.ts +61 -2
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +75 -18
- package/dist/testing/round_trip.d.ts +17 -0
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +67 -24
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +13 -138
- package/dist/testing/rpc_envelope_cases.d.ts +111 -0
- package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
- package/dist/testing/rpc_envelope_cases.js +240 -0
- package/dist/testing/rpc_helpers.d.ts +31 -0
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +35 -14
- package/dist/testing/rpc_round_trip.d.ts +62 -6
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +151 -39
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +22 -2
- package/dist/testing/standard.d.ts +26 -0
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +4 -1
- package/dist/testing/transports/raw_http.d.ts +21 -0
- package/dist/testing/transports/raw_http.d.ts.map +1 -1
- package/dist/testing/transports/raw_http.js +30 -0
- package/package.json +6 -4
- package/dist/testing/cross_backend/actor_search.d.ts +0 -6
- package/dist/testing/cross_backend/actor_search.d.ts.map +0 -1
- package/dist/testing/cross_backend/actor_search.js +0 -90
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +0 -1
|
@@ -31,9 +31,10 @@ import '../assert_dev_env.js';
|
|
|
31
31
|
* Rust spine) or one value joined from them (the TS runtimes), so a check that
|
|
32
32
|
* reads only the first lets it through.
|
|
33
33
|
* - **over cap + foreign `Origin`** declares a `Content-Length` one byte over
|
|
34
|
-
* the
|
|
35
|
-
*
|
|
36
|
-
* take a body have
|
|
34
|
+
* the route's cap — the shared 1 MiB default, or `rpc_max_body_bytes` on the
|
|
35
|
+
* RPC row — and carries no credential: the cap answers on the header, ahead
|
|
36
|
+
* of both the origin check and auth. Only the surfaces that take a body have
|
|
37
|
+
* the column.
|
|
37
38
|
* - **no credential** sends an allowed `Origin` and nothing else.
|
|
38
39
|
* - **narrowed token** sends a bearer minted with a method-scoped scope, and no
|
|
39
40
|
* `Origin` (a bearer in a browser context is discarded, not refused). The RPC
|
|
@@ -56,21 +57,31 @@ import '../assert_dev_env.js';
|
|
|
56
57
|
* in-process transport, whole bodies included, and the WebSocket rows skip
|
|
57
58
|
* (there is no socket to upgrade).
|
|
58
59
|
*
|
|
60
|
+
* **Multi-actor upgrade.** One more WS row needs a keeper seeded with a
|
|
61
|
+
* second actor (`extra_actors`), so it runs only given `multi_actor`: a
|
|
62
|
+
* multi-actor keeper's session upgrading with no `?acting=` → 400
|
|
63
|
+
* `actor_required`, the flat body the TS spine's upgrade-time authorization
|
|
64
|
+
* phase answers. The upgrade resolves an actor there, and a multi-actor
|
|
65
|
+
* account must name one. A backend whose upgrade resolves no actor admits
|
|
66
|
+
* the socket instead; `multi_actor.xfail` runs the row as `xfail_until` for
|
|
67
|
+
* it.
|
|
68
|
+
*
|
|
59
69
|
* Wire it for any backend that serves the spine's account routes, RPC
|
|
60
70
|
* endpoint, WebSocket endpoint and audit stream; the WS and SSE rows gate on
|
|
61
71
|
* `capabilities.ws` and `capabilities.sse`.
|
|
62
72
|
*
|
|
63
73
|
* @module
|
|
64
74
|
*/
|
|
65
|
-
import { assert, describe } from 'vitest';
|
|
75
|
+
import { assert, describe, test } from 'vitest';
|
|
66
76
|
import { randomBytes } from 'node:crypto';
|
|
67
77
|
import { account_verify_action_spec } from '../../auth/account_action_specs.js';
|
|
68
|
-
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_FORBIDDEN_ORIGIN, ERROR_PAYLOAD_TOO_LARGE, ERROR_TOKEN_SCOPE_REQUIRED } from '../../http/error_schemas.js';
|
|
78
|
+
import { ERROR_ACTOR_REQUIRED, ERROR_AUTHENTICATION_REQUIRED, ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_FORBIDDEN_ORIGIN, ERROR_PAYLOAD_TOO_LARGE, ERROR_TOKEN_SCOPE_REQUIRED } from '../../http/error_schemas.js';
|
|
69
79
|
import { create_rpc_get_url } from '../rpc_helpers.js';
|
|
70
|
-
import { raw_http_exchange, raw_http_parse_response } from '../transports/raw_http.js';
|
|
80
|
+
import { raw_http_exchange, raw_http_header_lines, raw_http_parse_json, raw_http_parse_response, RAW_HTTP_READ_TIMEOUT_MS } from '../transports/raw_http.js';
|
|
71
81
|
import { test_if } from './capabilities.js';
|
|
72
82
|
import { mint_narrowed_api_token } from './narrowed_api_token.js';
|
|
73
|
-
import { SPINE_RPC_PATH, SPINE_SSE_PATH } from './spine_surface_constants.js';
|
|
83
|
+
import { SPINE_ACCOUNT_LOGOUT_PATH, SPINE_ACCOUNT_PASSWORD_PATH, SPINE_BODY_LIMIT_DEFAULT_BYTES, SPINE_RPC_PATH, SPINE_SSE_PATH, SPINE_WS_PATH } from './spine_surface_constants.js';
|
|
84
|
+
import { xfail_until } from './xfail.js';
|
|
74
85
|
/** The RPC method the narrowed token admits. */
|
|
75
86
|
const NARROWED_ADMITTED_METHOD = account_verify_action_spec.method;
|
|
76
87
|
/**
|
|
@@ -146,29 +157,26 @@ export const transport_composition_cases = [
|
|
|
146
157
|
{ surface: 'sse', probe: 'no_credential', expect: UNAUTHENTICATED, gate: 'sse' },
|
|
147
158
|
{ surface: 'sse', probe: 'narrowed_token', expect: SESSION_ONLY, gate: 'sse' }
|
|
148
159
|
];
|
|
149
|
-
/**
|
|
150
|
-
|
|
160
|
+
/**
|
|
161
|
+
* The rows that need a multi-actor keeper — run only given
|
|
162
|
+
* `TransportCompositionCrossTestOptions.multi_actor`, on its fixture.
|
|
163
|
+
*/
|
|
164
|
+
export const transport_composition_multi_actor_cases = [
|
|
165
|
+
{
|
|
166
|
+
surface: 'ws_upgrade',
|
|
167
|
+
probe: 'multi_actor_no_acting',
|
|
168
|
+
expect: { status: 400, error: ERROR_ACTOR_REQUIRED },
|
|
169
|
+
gate: 'ws'
|
|
170
|
+
}
|
|
171
|
+
];
|
|
151
172
|
/** An `Origin` no backend's allowlist admits. */
|
|
152
173
|
const DISALLOWED_ORIGIN = 'http://evil.com';
|
|
153
|
-
/** How long a raw exchange waits for the refusal. */
|
|
154
|
-
const RAW_READ_TIMEOUT_MS = 2000;
|
|
155
174
|
const rpc_envelope = (method, id) => JSON.stringify({ jsonrpc: '2.0', method, id });
|
|
156
175
|
/** A JSON object body of exactly `bytes` ASCII bytes. */
|
|
157
176
|
const sized_json = (bytes) => {
|
|
158
177
|
const base = JSON.stringify({ pad: '' });
|
|
159
178
|
return JSON.stringify({ pad: 'x'.repeat(Math.max(0, bytes - base.length)) });
|
|
160
179
|
};
|
|
161
|
-
const to_header_lines = (headers) => Object.entries(headers)
|
|
162
|
-
.map(([name, value]) => `${name}: ${value}\r\n`)
|
|
163
|
-
.join('');
|
|
164
|
-
const parse_json = (text) => {
|
|
165
|
-
try {
|
|
166
|
-
return JSON.parse(text);
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
180
|
const case_name = (c) => `${c.surface} × ${c.probe} → ${c.expect.status} ${c.expect.error}`;
|
|
173
181
|
/**
|
|
174
182
|
* Read the refusal reason and fields out of a body, by the shape the case
|
|
@@ -207,9 +215,10 @@ const assert_refusal_body = (c, body, text) => {
|
|
|
207
215
|
export const describe_transport_composition_cross_tests = (options) => {
|
|
208
216
|
const { setup_test, capabilities, base_url } = options;
|
|
209
217
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
210
|
-
const rest_path = options.rest_path ??
|
|
211
|
-
const
|
|
212
|
-
const
|
|
218
|
+
const rest_path = options.rest_path ?? SPINE_ACCOUNT_LOGOUT_PATH;
|
|
219
|
+
const rpc_max_body_bytes = options.rpc_max_body_bytes ?? SPINE_BODY_LIMIT_DEFAULT_BYTES;
|
|
220
|
+
const password_path = options.password_path ?? SPINE_ACCOUNT_PASSWORD_PATH;
|
|
221
|
+
const ws_path = options.ws_path ?? SPINE_WS_PATH;
|
|
213
222
|
const sse_path = options.sse_path ?? SPINE_SSE_PATH;
|
|
214
223
|
const allowed_origin = options.origin ?? (base_url === null ? 'http://localhost:5173' : new URL(base_url).origin);
|
|
215
224
|
/** The request line, headers and body a case's probe sends. */
|
|
@@ -236,6 +245,10 @@ export const describe_transport_composition_cross_tests = (options) => {
|
|
|
236
245
|
case 'no_credential':
|
|
237
246
|
credential = { origin: allowed_origin };
|
|
238
247
|
break;
|
|
248
|
+
case 'multi_actor_no_acting':
|
|
249
|
+
// the multi-actor keeper's session, no `?acting=` on the path
|
|
250
|
+
credential = { ...fixture.create_session_headers(), origin: allowed_origin };
|
|
251
|
+
break;
|
|
239
252
|
case 'narrowed_token': {
|
|
240
253
|
const token = await mint_narrowed_api_token({
|
|
241
254
|
app: fixture.transport,
|
|
@@ -257,8 +270,8 @@ export const describe_transport_composition_cross_tests = (options) => {
|
|
|
257
270
|
method: 'POST',
|
|
258
271
|
path: c.surface === 'rest_post' ? rest_path : password_path,
|
|
259
272
|
headers: { ...credential, 'content-type': 'application/json' },
|
|
260
|
-
body: over_cap ? sized_json(
|
|
261
|
-
over_cap,
|
|
273
|
+
body: over_cap ? sized_json(SPINE_BODY_LIMIT_DEFAULT_BYTES + 1) : '{}',
|
|
274
|
+
over_cap_length: over_cap ? SPINE_BODY_LIMIT_DEFAULT_BYTES + 1 : null,
|
|
262
275
|
repeated_headers
|
|
263
276
|
};
|
|
264
277
|
case 'rpc_post': {
|
|
@@ -266,8 +279,8 @@ export const describe_transport_composition_cross_tests = (options) => {
|
|
|
266
279
|
method: 'POST',
|
|
267
280
|
path: rpc_path,
|
|
268
281
|
headers: { ...credential, 'content-type': 'application/json' },
|
|
269
|
-
body: over_cap ? sized_json(
|
|
270
|
-
over_cap,
|
|
282
|
+
body: over_cap ? sized_json(rpc_max_body_bytes + 1) : rpc_envelope(rpc_method, id),
|
|
283
|
+
over_cap_length: over_cap ? rpc_max_body_bytes + 1 : null,
|
|
271
284
|
repeated_headers
|
|
272
285
|
};
|
|
273
286
|
}
|
|
@@ -277,7 +290,7 @@ export const describe_transport_composition_cross_tests = (options) => {
|
|
|
277
290
|
path: create_rpc_get_url(rpc_path, rpc_method, undefined, id),
|
|
278
291
|
headers: credential,
|
|
279
292
|
body: null,
|
|
280
|
-
|
|
293
|
+
over_cap_length: null,
|
|
281
294
|
repeated_headers
|
|
282
295
|
};
|
|
283
296
|
case 'ws_upgrade':
|
|
@@ -292,7 +305,7 @@ export const describe_transport_composition_cross_tests = (options) => {
|
|
|
292
305
|
'sec-websocket-version': '13'
|
|
293
306
|
},
|
|
294
307
|
body: null,
|
|
295
|
-
|
|
308
|
+
over_cap_length: null,
|
|
296
309
|
repeated_headers
|
|
297
310
|
};
|
|
298
311
|
case 'sse':
|
|
@@ -301,7 +314,7 @@ export const describe_transport_composition_cross_tests = (options) => {
|
|
|
301
314
|
path: sse_path,
|
|
302
315
|
headers: { ...credential, accept: 'text/event-stream' },
|
|
303
316
|
body: null,
|
|
304
|
-
|
|
317
|
+
over_cap_length: null,
|
|
305
318
|
repeated_headers
|
|
306
319
|
};
|
|
307
320
|
}
|
|
@@ -321,44 +334,61 @@ export const describe_transport_composition_cross_tests = (options) => {
|
|
|
321
334
|
return { status: res.status, text: await res.text() };
|
|
322
335
|
}
|
|
323
336
|
const url = new URL(base_url);
|
|
324
|
-
const body = probe.
|
|
325
|
-
const length_header = probe.
|
|
326
|
-
? { 'content-length': String(
|
|
337
|
+
const body = probe.over_cap_length === null ? (probe.body ?? '') : '';
|
|
338
|
+
const length_header = probe.over_cap_length !== null
|
|
339
|
+
? { 'content-length': String(probe.over_cap_length) }
|
|
327
340
|
: probe.body === null
|
|
328
341
|
? {}
|
|
329
342
|
: { 'content-length': String(body.length) };
|
|
330
343
|
const request = `${probe.method} ${probe.path} HTTP/1.1\r\n` +
|
|
331
|
-
|
|
344
|
+
raw_http_header_lines({ host: url.host, ...probe.headers, ...length_header }) +
|
|
332
345
|
(probe.repeated_headers ?? []).map(([name, value]) => `${name}: ${value}\r\n`).join('') +
|
|
333
346
|
`\r\n` +
|
|
334
347
|
body;
|
|
335
348
|
const { received, closed_by_server } = await raw_http_exchange(base_url, request, {
|
|
336
|
-
read_timeout_ms:
|
|
349
|
+
read_timeout_ms: RAW_HTTP_READ_TIMEOUT_MS,
|
|
337
350
|
until: 'response'
|
|
338
351
|
});
|
|
339
352
|
const response = raw_http_parse_response(received);
|
|
340
353
|
assert.ok(response, `${probe.method} ${probe.path}: expected a whole response; ` +
|
|
341
354
|
(closed_by_server
|
|
342
355
|
? 'the server closed the connection first'
|
|
343
|
-
: `none arrived within ${
|
|
356
|
+
: `none arrived within ${RAW_HTTP_READ_TIMEOUT_MS}ms`) +
|
|
344
357
|
`. Received: ${received ? received.slice(0, 200) : 'nothing'}`);
|
|
345
358
|
return { status: response.status, text: response.body };
|
|
346
359
|
};
|
|
360
|
+
const run_case = async (c, fixture_setup) => {
|
|
361
|
+
const fixture = await fixture_setup();
|
|
362
|
+
const res = await send_probe(fixture, await build_probe(c, fixture));
|
|
363
|
+
assert.strictEqual(res.status, c.expect.status, `${case_name(c)}: status (body: ${res.text.slice(0, 200)})`);
|
|
364
|
+
// a backend that declares no WS refusal body (Node's `@hono/node-ws`)
|
|
365
|
+
// may answer with an empty one; a body it does send is still asserted
|
|
366
|
+
if (c.surface === 'ws_upgrade' && !capabilities.ws_refusal_body && res.text === '')
|
|
367
|
+
return;
|
|
368
|
+
assert_refusal_body(c, raw_http_parse_json(res.text), res.text);
|
|
369
|
+
};
|
|
370
|
+
const is_runnable = (c) => (c.gate === undefined || capabilities[c.gate]) &&
|
|
371
|
+
// an upgrade needs a socket; in-process there is none
|
|
372
|
+
(c.surface !== 'ws_upgrade' || base_url !== null);
|
|
347
373
|
describe('transport composition (front door)', () => {
|
|
348
374
|
for (const c of transport_composition_cases) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
375
|
+
test_if(is_runnable(c), case_name(c), () => run_case(c, setup_test));
|
|
376
|
+
}
|
|
377
|
+
const { multi_actor } = options;
|
|
378
|
+
if (!multi_actor)
|
|
379
|
+
return;
|
|
380
|
+
for (const c of transport_composition_multi_actor_cases) {
|
|
381
|
+
const name = case_name(c);
|
|
382
|
+
const body = () => run_case(c, multi_actor.setup_test);
|
|
383
|
+
if (!is_runnable(c)) {
|
|
384
|
+
test_if(false, name, body);
|
|
385
|
+
}
|
|
386
|
+
else if (multi_actor.xfail) {
|
|
387
|
+
xfail_until(multi_actor.xfail.tracking_id, multi_actor.xfail.reason, name, body);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
test(name, body);
|
|
391
|
+
}
|
|
362
392
|
}
|
|
363
393
|
});
|
|
364
394
|
};
|
|
@@ -34,6 +34,14 @@ export interface TsSpineBackendConfigOptions {
|
|
|
34
34
|
readonly port?: number;
|
|
35
35
|
/** Database URL. Default `'memory://'` (in-memory pglet). */
|
|
36
36
|
readonly database_url?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Backend name — labels the spawn in errors and logs, and names its
|
|
39
|
+
* directory under `os.tmpdir()` (`build_test_backend_paths`). Defaults per
|
|
40
|
+
* runtime (`ts_spine_node`, `ts_spine_deno`, `ts_spine_bun`). Give each
|
|
41
|
+
* backend spawned alongside another of its runtime its own name, so their
|
|
42
|
+
* bootstrap and daemon-token files don't collide.
|
|
43
|
+
*/
|
|
44
|
+
readonly name?: string;
|
|
37
45
|
/**
|
|
38
46
|
* Enable the per-IP + per-account login rate limiters on the spawned binary
|
|
39
47
|
* (`FUZ_LOGIN_RATE_LIMIT_ENABLED=true`). Off by default — the standard cross
|
|
@@ -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;
|
|
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;AA4EzD,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,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAwBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAwBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAqCF,CAAC"}
|
|
@@ -3,7 +3,12 @@ import { build_test_backend_paths } from './build_test_backend_paths.js';
|
|
|
3
3
|
import { action_rate_limit_env, make_default_ts_backend_config, ts_default_capabilities } from './default_backend_configs.js';
|
|
4
4
|
import { LOGIN_RATE_LIMIT_ENABLED_ENV, SPINE_SSE_PATH, TS_SPINE_DIR_ENV } from './spine_surface_constants.js';
|
|
5
5
|
/**
|
|
6
|
-
* Capabilities for the TS spine binary — `ts_default_capabilities`
|
|
6
|
+
* Capabilities for the TS spine binary — `ts_default_capabilities` with every `MountFlag` stated, all `true`: `ws`
|
|
7
|
+
* (the binary mounts `/api/ws` through `create_app_server`'s `ws_endpoints`),
|
|
8
|
+
* `cell_crud` + `cell_relations` (`full_spine_mount` live-mounts the full cell
|
|
9
|
+
* surface over the `fuz_cell` namespace), `fact_serving` (the cell-gated serve
|
|
10
|
+
* route, the `fuz_facts` namespace and `_testing_put_fact`),
|
|
11
|
+
* `fact_serving_admin_hash` (the admin bare-hash route), `sse`
|
|
7
12
|
* (the binary wires `audit_log_sse`), `ready` (the binary live-mounts the
|
|
8
13
|
* `/ready` deploy gate in `build_spine_app`), `peer_request` (the backend
|
|
9
14
|
* WS transport's `request_connection` path drives server→client `peer/ping`),
|
|
@@ -17,7 +22,12 @@ import { LOGIN_RATE_LIMIT_ENABLED_ENV, SPINE_SSE_PATH, TS_SPINE_DIR_ENV } from '
|
|
|
17
22
|
*/
|
|
18
23
|
const ts_spine_capabilities = Object.freeze({
|
|
19
24
|
...ts_default_capabilities,
|
|
25
|
+
ws: true,
|
|
20
26
|
sse: true,
|
|
27
|
+
cell_crud: true,
|
|
28
|
+
cell_relations: true,
|
|
29
|
+
fact_serving: true,
|
|
30
|
+
fact_serving_admin_hash: true,
|
|
21
31
|
ready: true,
|
|
22
32
|
peer_request: true,
|
|
23
33
|
ws_account_actions: true,
|
|
@@ -75,8 +85,7 @@ const login_rate_limit_env = (enable) => enable ? { [LOGIN_RATE_LIMIT_ENABLED_EN
|
|
|
75
85
|
* (Gro's TS loader resolves the entry's relative imports).
|
|
76
86
|
*/
|
|
77
87
|
export const ts_spine_node_backend_config = (options = {}) => {
|
|
78
|
-
const { port = TS_SPINE_NODE_DEFAULT_PORT, database_url, enable_login_rate_limit } = options;
|
|
79
|
-
const name = 'ts_spine_node';
|
|
88
|
+
const { port = TS_SPINE_NODE_DEFAULT_PORT, database_url, enable_login_rate_limit, name = 'ts_spine_node' } = options;
|
|
80
89
|
const paths = build_test_backend_paths(name);
|
|
81
90
|
return {
|
|
82
91
|
...make_default_ts_backend_config({
|
|
@@ -85,8 +94,8 @@ export const ts_spine_node_backend_config = (options = {}) => {
|
|
|
85
94
|
start_command: ['gro', 'run', TS_SPINE_NODE_ENTRY],
|
|
86
95
|
database_url,
|
|
87
96
|
paths,
|
|
97
|
+
daemon_token_dir_env: TS_SPINE_DIR_ENV,
|
|
88
98
|
extra_env: {
|
|
89
|
-
[TS_SPINE_DIR_ENV]: paths.root,
|
|
90
99
|
...login_rate_limit_env(enable_login_rate_limit),
|
|
91
100
|
...action_rate_limit_env(options)
|
|
92
101
|
},
|
|
@@ -103,8 +112,7 @@ export const ts_spine_node_backend_config = (options = {}) => {
|
|
|
103
112
|
* optional `@hono/bun` peer dep.
|
|
104
113
|
*/
|
|
105
114
|
export const ts_spine_bun_backend_config = (options = {}) => {
|
|
106
|
-
const { port = TS_SPINE_BUN_DEFAULT_PORT, database_url, enable_login_rate_limit } = options;
|
|
107
|
-
const name = 'ts_spine_bun';
|
|
115
|
+
const { port = TS_SPINE_BUN_DEFAULT_PORT, database_url, enable_login_rate_limit, name = 'ts_spine_bun' } = options;
|
|
108
116
|
const paths = build_test_backend_paths(name);
|
|
109
117
|
return {
|
|
110
118
|
...make_default_ts_backend_config({
|
|
@@ -113,8 +121,8 @@ export const ts_spine_bun_backend_config = (options = {}) => {
|
|
|
113
121
|
start_command: ['bun', 'run', TS_SPINE_BUN_ENTRY],
|
|
114
122
|
database_url,
|
|
115
123
|
paths,
|
|
124
|
+
daemon_token_dir_env: TS_SPINE_DIR_ENV,
|
|
116
125
|
extra_env: {
|
|
117
|
-
[TS_SPINE_DIR_ENV]: paths.root,
|
|
118
126
|
...login_rate_limit_env(enable_login_rate_limit),
|
|
119
127
|
...action_rate_limit_env(options)
|
|
120
128
|
},
|
|
@@ -135,8 +143,7 @@ export const ts_spine_bun_backend_config = (options = {}) => {
|
|
|
135
143
|
* built package; this binary tests live source instead.)
|
|
136
144
|
*/
|
|
137
145
|
export const ts_spine_deno_backend_config = (options = {}) => {
|
|
138
|
-
const { port = TS_SPINE_DENO_DEFAULT_PORT, database_url, enable_login_rate_limit } = options;
|
|
139
|
-
const name = 'ts_spine_deno';
|
|
146
|
+
const { port = TS_SPINE_DENO_DEFAULT_PORT, database_url, enable_login_rate_limit, name = 'ts_spine_deno' } = options;
|
|
140
147
|
const paths = build_test_backend_paths(name);
|
|
141
148
|
return {
|
|
142
149
|
...make_default_ts_backend_config({
|
|
@@ -158,8 +165,8 @@ export const ts_spine_deno_backend_config = (options = {}) => {
|
|
|
158
165
|
],
|
|
159
166
|
database_url,
|
|
160
167
|
paths,
|
|
168
|
+
daemon_token_dir_env: TS_SPINE_DIR_ENV,
|
|
161
169
|
extra_env: {
|
|
162
|
-
[TS_SPINE_DIR_ENV]: paths.root,
|
|
163
170
|
...login_rate_limit_env(enable_login_rate_limit),
|
|
164
171
|
...action_rate_limit_env(options)
|
|
165
172
|
},
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
2
|
import type { SetupTest } from './setup.js';
|
|
3
|
+
import { type WsEndpointTarget } from './ws_endpoint.js';
|
|
3
4
|
/** Options for `describe_ws_action_rate_limit_cross_tests`. */
|
|
4
|
-
export interface WsActionRateLimitCrossTestOptions {
|
|
5
|
+
export interface WsActionRateLimitCrossTestOptions extends WsEndpointTarget {
|
|
5
6
|
/** Per-test fixture producer (cross-process only — see the module doc). */
|
|
6
7
|
readonly setup_test: SetupTest;
|
|
7
|
-
/** Base URL the backend is reachable at (e.g. `http://localhost:1177`). */
|
|
8
|
-
readonly base_url: string;
|
|
9
|
-
/** WebSocket endpoint path (e.g. `/api/ws`). */
|
|
10
|
-
readonly ws_path: string;
|
|
11
8
|
/** JSON-RPC endpoint path. Default `/api/rpc` (the spine convention). */
|
|
12
9
|
readonly rpc_path?: string;
|
|
13
10
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_action_rate_limit.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_action_rate_limit.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"ws_action_rate_limit.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_action_rate_limit.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiE9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAgD1E,+DAA+D;AAC/D,MAAM,WAAW,iCAAkC,SAAQ,gBAAgB;IAC1E,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAkBD;;;;GAIG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,iCAAiC,KACxC,IAkSF,CAAC"}
|
|
@@ -54,8 +54,9 @@ import '../assert_dev_env.js';
|
|
|
54
54
|
*/
|
|
55
55
|
import { assert, describe, test } from 'vitest';
|
|
56
56
|
import { create_jsonrpc_request } from '../../http/jsonrpc_helpers.js';
|
|
57
|
-
import { create_ws_transport } from '../transports/ws_transport.js';
|
|
58
57
|
import { is_response_for } from '../transports/ws_client.js';
|
|
58
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
59
|
+
import { open_session_ws } from './ws_endpoint.js';
|
|
59
60
|
// Wire literals are deliberate independent copies of the producers'
|
|
60
61
|
// constants (`JSONRPC_ERROR_CODES` on TS, the `JsonrpcError` taxonomy on
|
|
61
62
|
// Rust) — parity by test, so a renamed or renumbered code fails here.
|
|
@@ -106,8 +107,8 @@ const outcome_from_frame = (status, frame) => ({
|
|
|
106
107
|
* across both transports, and the account axis at the RPC and WS dispatchers.
|
|
107
108
|
*/
|
|
108
109
|
export const describe_ws_action_rate_limit_cross_tests = (options) => {
|
|
109
|
-
const { setup_test,
|
|
110
|
-
const rpc_path = options.rpc_path ??
|
|
110
|
+
const { setup_test, max_attempts } = options;
|
|
111
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
111
112
|
assert(Number.isInteger(max_attempts) && max_attempts >= 2, 'ws_action_rate_limit needs max_attempts >= 2 to split a budget across transports');
|
|
112
113
|
let next_id = 1;
|
|
113
114
|
/** POST one JSON-RPC call on a fresh (cookie-jar-free) transport. */
|
|
@@ -128,17 +129,10 @@ export const describe_ws_action_rate_limit_cross_tests = (options) => {
|
|
|
128
129
|
return outcome_from_frame(null, frame);
|
|
129
130
|
};
|
|
130
131
|
/** Open a socket on the keeper's session, optionally behind a forwarded IP. */
|
|
131
|
-
const open_ws = (fixture, forwarded_for) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
base_url,
|
|
136
|
-
ws_path,
|
|
137
|
-
cookies: [cookie],
|
|
138
|
-
origin: base_url,
|
|
139
|
-
headers: forwarded_for ? { 'x-forwarded-for': forwarded_for } : undefined
|
|
140
|
-
});
|
|
141
|
-
};
|
|
132
|
+
const open_ws = (fixture, forwarded_for) => open_session_ws(options, fixture, {
|
|
133
|
+
admitted: false,
|
|
134
|
+
headers: forwarded_for ? { 'x-forwarded-for': forwarded_for } : undefined
|
|
135
|
+
});
|
|
142
136
|
const assert_charged_not_limited = (outcome, label) => {
|
|
143
137
|
assert.strictEqual(outcome.code, CODE_INVALID_PARAMS, `${label}: expected invalid_params (charged, under the cap), got ${JSON.stringify(outcome)}`);
|
|
144
138
|
if (outcome.status !== null)
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
2
|
import { type BackendCapabilities } from './capabilities.js';
|
|
3
3
|
import type { SetupTest } from './setup.js';
|
|
4
|
+
import { type WsEndpointTarget } from './ws_endpoint.js';
|
|
4
5
|
/** Options for the WebSocket connection-cap parity suite. */
|
|
5
|
-
export interface WsConnectionCapCrossTestOptions {
|
|
6
|
+
export interface WsConnectionCapCrossTestOptions extends WsEndpointTarget {
|
|
6
7
|
/**
|
|
7
8
|
* Per-test fixture producer (`default_cross_process_setup(handle)`). Every
|
|
8
|
-
* socket in a case upgrades
|
|
9
|
-
*
|
|
9
|
+
* socket in a case upgrades on the fresh-per-test keeper's session
|
|
10
|
+
* cookie, so they all land on one account.
|
|
10
11
|
*/
|
|
11
12
|
readonly setup_test: SetupTest;
|
|
12
13
|
/** Backend capability flags; every case gates on `capabilities.ws`. */
|
|
13
14
|
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
15
|
/**
|
|
21
16
|
* The backend's per-account connection cap, which each case opens past.
|
|
22
17
|
* Defaults to `DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT`, the cap both spines
|
|
@@ -1 +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;
|
|
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;AAkD9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AACzD,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAK1E,6DAA6D;AAC7D,MAAM,WAAW,+BAAgC,SAAQ,gBAAgB;IACxE;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,2BAA2B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,+BAA+B,KACtC,IA2GF,CAAC"}
|
|
@@ -44,8 +44,8 @@ import { assert, describe } from 'vitest';
|
|
|
44
44
|
import { heartbeat_action_spec } from '../../actions/heartbeat.js';
|
|
45
45
|
import { WS_CLOSE_CONNECTION_LIMIT } from '../../actions/transports.js';
|
|
46
46
|
import { DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT } from '../../actions/transports_ws_backend.js';
|
|
47
|
-
import { create_admitted_ws_transport } from '../transports/ws_transport.js';
|
|
48
47
|
import { test_if } from './capabilities.js';
|
|
48
|
+
import { open_session_ws } from './ws_endpoint.js';
|
|
49
49
|
/** The close reason both spines send with `WS_CLOSE_CONNECTION_LIMIT`. */
|
|
50
50
|
const CONNECTION_LIMIT_REASON = 'connection limit';
|
|
51
51
|
/**
|
|
@@ -55,7 +55,7 @@ const CONNECTION_LIMIT_REASON = 'connection limit';
|
|
|
55
55
|
* frees its slot.
|
|
56
56
|
*/
|
|
57
57
|
export const describe_ws_connection_cap_cross_tests = (options) => {
|
|
58
|
-
const { setup_test, capabilities
|
|
58
|
+
const { setup_test, capabilities } = options;
|
|
59
59
|
const max = options.max_connections_per_account === undefined
|
|
60
60
|
? DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT
|
|
61
61
|
: options.max_connections_per_account;
|
|
@@ -64,7 +64,7 @@ export const describe_ws_connection_cap_cross_tests = (options) => {
|
|
|
64
64
|
* Open a socket the backend has admitted (a `heartbeat` round trip), so
|
|
65
65
|
* the connections are registered in the order they were opened.
|
|
66
66
|
*/
|
|
67
|
-
const open_registered = (
|
|
67
|
+
const open_registered = (fixture) => open_session_ws(options, fixture, { admitted: true });
|
|
68
68
|
const assert_superseded = async (client, label) => {
|
|
69
69
|
const closed = await client.wait_for_close(2000);
|
|
70
70
|
assert.ok(closed, `${label} did not close within 2s of the cap overflow`);
|
|
@@ -78,27 +78,27 @@ export const describe_ws_connection_cap_cross_tests = (options) => {
|
|
|
78
78
|
assert.deepStrictEqual(clients.map((client) => client.close_code), clients.map(() => null), message);
|
|
79
79
|
};
|
|
80
80
|
// each case opens more than a cap's worth of real sockets in sequence, so its
|
|
81
|
-
// time scales with the cap and the backend's upgrade cost
|
|
82
|
-
|
|
81
|
+
// time scales with the cap and the backend's upgrade cost — inside the 30 s
|
|
82
|
+
// budget `make_cross_backend_project` gives every case
|
|
83
|
+
describe('per-account websocket connection cap parity', () => {
|
|
83
84
|
test_if(enabled, 'one connection past the cap closes the oldest with WS_CLOSE_CONNECTION_LIMIT, and only it', async () => {
|
|
84
85
|
const fixture = await setup_test();
|
|
85
|
-
const cookies = fixture.transport.cookies();
|
|
86
86
|
const sockets = [];
|
|
87
87
|
try {
|
|
88
88
|
for (let i = 0; i < max; i++) {
|
|
89
|
-
sockets.push(await open_registered(
|
|
89
|
+
sockets.push(await open_registered(fixture));
|
|
90
90
|
}
|
|
91
91
|
// at the cap, nothing has closed
|
|
92
92
|
await assert_all_answer(sockets, 'at_cap', 'every socket at the cap stays open');
|
|
93
93
|
// one past it: always admitted, never refused
|
|
94
|
-
sockets.push(await open_registered(
|
|
94
|
+
sockets.push(await open_registered(fixture));
|
|
95
95
|
const [oldest, ...survivors] = sockets;
|
|
96
96
|
await assert_superseded(oldest, 'the oldest socket');
|
|
97
97
|
await assert_all_answer(survivors, 'past_cap', 'only the oldest may close; every other socket, the newest included, keeps answering');
|
|
98
98
|
// Not a revocation — the session still authenticates an upgrade
|
|
99
99
|
// (a revoked one is refused there). That upgrade is one more past
|
|
100
100
|
// the cap, so it supersedes the next-oldest.
|
|
101
|
-
sockets.push(await open_registered(
|
|
101
|
+
sockets.push(await open_registered(fixture));
|
|
102
102
|
await assert_superseded(survivors[0], 'the next-oldest socket');
|
|
103
103
|
await assert_all_answer(sockets.slice(2), 'past_cap_again', 'the session keeps its newest sockets after a second overflow');
|
|
104
104
|
}
|
|
@@ -108,14 +108,13 @@ export const describe_ws_connection_cap_cross_tests = (options) => {
|
|
|
108
108
|
});
|
|
109
109
|
test_if(enabled, 'a closed connection frees its slot', async () => {
|
|
110
110
|
const fixture = await setup_test();
|
|
111
|
-
const
|
|
112
|
-
const held = await open_registered(cookies);
|
|
111
|
+
const held = await open_registered(fixture);
|
|
113
112
|
try {
|
|
114
113
|
// A cap's worth of connections, one at a time, each closed before
|
|
115
114
|
// the next opens — the account never holds more than a few at once,
|
|
116
115
|
// however late the backend notices each close.
|
|
117
116
|
for (let i = 0; i < max; i++) {
|
|
118
|
-
const client = await open_registered(
|
|
117
|
+
const client = await open_registered(fixture);
|
|
119
118
|
await client.close();
|
|
120
119
|
}
|
|
121
120
|
await assert_all_answer([held], 'held', 'closed connections must not count toward the cap — the held socket was evicted');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import { type WsTransportOptions } from '../transports/ws_transport.js';
|
|
3
|
+
import type { WsClient } from '../transports/ws_client.js';
|
|
4
|
+
/** Where a backend's WebSocket endpoint is. */
|
|
5
|
+
export interface WsEndpointTarget {
|
|
6
|
+
/** Base URL the backend is reachable at (e.g. `http://localhost:1178`). */
|
|
7
|
+
readonly base_url: string;
|
|
8
|
+
/** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
|
|
9
|
+
readonly ws_path: string;
|
|
10
|
+
/** `Origin` for a browser-context upgrade. Defaults to `base_url`. */
|
|
11
|
+
readonly origin?: string;
|
|
12
|
+
}
|
|
13
|
+
/** An account a session socket opens as — a fixture or a minted account. */
|
|
14
|
+
export interface WsSessionAccount {
|
|
15
|
+
/** Build request headers carrying the account's session cookie. */
|
|
16
|
+
readonly create_session_headers: (extra?: Record<string, string>) => Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
/** Options for `open_session_ws`. */
|
|
19
|
+
export interface OpenSessionWsOptions extends Pick<WsTransportOptions, 'default_timeout_ms' | 'on_request' | 'headers'> {
|
|
20
|
+
/**
|
|
21
|
+
* Round-trip `heartbeat` before resolving, so the backend has admitted the
|
|
22
|
+
* connection (`create_admitted_ws_transport`) — needed by a case that waits
|
|
23
|
+
* for a push or relies on registration order. `false` resolves once the
|
|
24
|
+
* socket is open (`create_ws_transport`), for a case that only round-trips
|
|
25
|
+
* or counts its own requests.
|
|
26
|
+
*/
|
|
27
|
+
readonly admitted: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The account's session cookie (`<name>=<value>`), asserted present.
|
|
31
|
+
*
|
|
32
|
+
* @param account - the account whose session headers carry the cookie
|
|
33
|
+
* @returns the `Cookie` header value
|
|
34
|
+
*/
|
|
35
|
+
export declare const to_session_cookie: (account: WsSessionAccount) => string;
|
|
36
|
+
/**
|
|
37
|
+
* Open a real-upgrade socket on `target` as `account`'s session: its session
|
|
38
|
+
* cookie on the upgrade, `target.origin` (or `base_url`) as the `Origin`.
|
|
39
|
+
*
|
|
40
|
+
* The cookie is the static one `account.create_session_headers` carries, not
|
|
41
|
+
* whatever `fixture.transport`'s jar holds now, so a case that rotates the
|
|
42
|
+
* session through `fixture.transport` first upgrades on the stale cookie.
|
|
43
|
+
*
|
|
44
|
+
* @param target - the backend's WebSocket endpoint
|
|
45
|
+
* @param account - the account whose session the socket upgrades on
|
|
46
|
+
* @param options - whether to wait for admission, plus transport extras
|
|
47
|
+
* @returns the open socket
|
|
48
|
+
* @throws Error if the account carries no session cookie, or the upgrade (or
|
|
49
|
+
* the admission heartbeat) fails
|
|
50
|
+
*/
|
|
51
|
+
export declare const open_session_ws: (target: WsEndpointTarget, account: WsSessionAccount, options: OpenSessionWsOptions) => Promise<WsClient>;
|
|
52
|
+
//# sourceMappingURL=ws_endpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_endpoint.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B,OAAO,EAGN,KAAK,kBAAkB,EACvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAE3D,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAChC,mEAAmE;IACnE,QAAQ,CAAC,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5F;AAED,qCAAqC;AACrC,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CACjD,kBAAkB,EAClB,oBAAoB,GAAG,YAAY,GAAG,SAAS,CAC/C;IACA;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,gBAAgB,KAAG,MAI7D,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,GAC3B,QAAQ,gBAAgB,EACxB,SAAS,gBAAgB,EACzB,SAAS,oBAAoB,KAC3B,OAAO,CAAC,QAAQ,CAUlB,CAAC"}
|