@fuzdev/fuz_app 0.147.0 → 0.149.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 +8 -6
- 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/action_event.d.ts.map +1 -1
- package/dist/actions/action_event.js +14 -1
- package/dist/actions/perform_action.d.ts +17 -1
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +23 -3
- package/dist/auth/CLAUDE.md +3 -2
- package/dist/auth/account_action_specs.d.ts +12 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +16 -2
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +7 -4
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +29 -8
- package/dist/auth/audit_emitter.d.ts +14 -0
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +5 -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 +8 -4
- package/dist/http/CLAUDE.md +14 -2
- package/dist/http/error_schemas.d.ts +8 -0
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -0
- package/dist/http/jsonrpc_errors.d.ts +40 -15
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +90 -35
- 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 +80 -0
- package/dist/schema_meta.d.ts.map +1 -1
- package/dist/schema_meta.js +71 -1
- package/dist/testing/CLAUDE.md +853 -146
- 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 +124 -0
- package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
- package/dist/testing/adversarial_input_cases.js +393 -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 +30 -3
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +55 -3
- 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 +57 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/audit_cross_helpers.js +89 -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 +78 -59
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +25 -24
- 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 +174 -3
- 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} +14 -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 +58 -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 +47 -0
- package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_adversarial.js +302 -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 +33 -21
- 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 +58 -56
- 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 +311 -0
- package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_suites.js +897 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +44 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +53 -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 +29 -12
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +143 -51
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +30 -14
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +58 -37
- 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 +23 -13
- 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 +10 -0
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +27 -140
- package/dist/testing/rpc_envelope_cases.d.ts +114 -0
- package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
- package/dist/testing/rpc_envelope_cases.js +243 -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 +99 -2
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +261 -73
- 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/stubs.d.ts +4 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- 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/dist/zod.d.ts +9 -0
- package/dist/zod.d.ts.map +1 -1
- package/dist/zod.js +20 -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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
2
|
import type { RouteSpec, RouteMethod } from '../http/route_spec.js';
|
|
3
|
+
import { type RouteAuth } from '../http/auth_shape.js';
|
|
3
4
|
import type { Keyring } from '../auth/keyring.js';
|
|
4
5
|
import { type SessionOptions } from '../auth/session_cookie.js';
|
|
5
6
|
import type { TestAccount } from './app_server.js';
|
|
@@ -100,9 +101,36 @@ export interface KeeperHeaderProvider {
|
|
|
100
101
|
create_daemon_token_headers: (extra?: Record<string, string>) => Record<string, string>;
|
|
101
102
|
}
|
|
102
103
|
/**
|
|
103
|
-
*
|
|
104
|
+
* Who a generated request calls as, picked from the declared auth: no
|
|
105
|
+
* credential for a public route or method, else the keeper's daemon token
|
|
106
|
+
* (`credential_types` names it), the admin account's session (an
|
|
107
|
+
* admin-gated one), the keeper's session (another role), or the authed
|
|
108
|
+
* account's session.
|
|
109
|
+
*/
|
|
110
|
+
export type AuthPrincipal = 'public' | 'daemon_token' | 'admin' | 'keeper' | 'authed';
|
|
111
|
+
/**
|
|
112
|
+
* Pick the principal a generated request calls as, from its declared auth.
|
|
113
|
+
* Shared by the REST and RPC round-trips (`pick_auth_headers`,
|
|
114
|
+
* `pick_rpc_auth`), so both call each auth shape as the same principal.
|
|
104
115
|
*
|
|
105
|
-
*
|
|
116
|
+
* Every principal but `'public'` carries a live credential of the kind the
|
|
117
|
+
* auth names, so the backend must authenticate it; a role mismatch (a keeper
|
|
118
|
+
* session on a role the keeper lacks) still refuses as 403, after
|
|
119
|
+
* authentication.
|
|
120
|
+
*/
|
|
121
|
+
export declare const pick_auth_principal: (auth: RouteAuth) => AuthPrincipal;
|
|
122
|
+
/**
|
|
123
|
+
* The request headers carrying a principal's credential.
|
|
124
|
+
*
|
|
125
|
+
* @param principal - the principal `pick_auth_principal` chose
|
|
126
|
+
* @param keeper - the keeper's header builders
|
|
127
|
+
* @param authed_account - the account the `'authed'` principal calls as
|
|
128
|
+
* @param admin_account - the account the `'admin'` principal calls as
|
|
129
|
+
*/
|
|
130
|
+
export declare const to_principal_headers: (principal: AuthPrincipal, keeper: KeeperHeaderProvider, authed_account: TestAccount, admin_account: TestAccount) => Record<string, string>;
|
|
131
|
+
/**
|
|
132
|
+
* Pick request headers matching a route spec's auth requirement
|
|
133
|
+
* (`pick_auth_principal`, then `to_principal_headers`):
|
|
106
134
|
* - `none` — origin headers only
|
|
107
135
|
* - `authenticated` — the authed account's session cookie
|
|
108
136
|
* - `role: admin` — the admin account's session cookie
|
|
@@ -110,4 +138,35 @@ export interface KeeperHeaderProvider {
|
|
|
110
138
|
* - `keeper` — the keeper provider's daemon token
|
|
111
139
|
*/
|
|
112
140
|
export declare const pick_auth_headers: (spec: RouteSpec, keeper: KeeperHeaderProvider, authed_account: TestAccount, admin_account: TestAccount) => Record<string, string>;
|
|
141
|
+
/**
|
|
142
|
+
* Whether a round-trip sends a request as a fresh account rather than the
|
|
143
|
+
* shared authed one: a side-effecting request picked for the `'authed'`
|
|
144
|
+
* principal. Generated input omits optional fields, and an omitted target on
|
|
145
|
+
* a self-service action is the caller — `account_delete` deletes it,
|
|
146
|
+
* `account_session_revoke_all`, a logout or a password change end its
|
|
147
|
+
* session — so any side-effecting call may destroy the credential it was sent
|
|
148
|
+
* with, and on a shared account every later call would refuse as
|
|
149
|
+
* unauthenticated. Keyed on the request's own side-effect signal (an action's
|
|
150
|
+
* `side_effects`, a route's non-`GET` method) rather than a list of known
|
|
151
|
+
* destroyers, so a consumer's self-targeting action is covered without
|
|
152
|
+
* registering it. The other principals stay shared: the keeper and its daemon
|
|
153
|
+
* token are one per fixture, and an admin-gated request with generated nil ids
|
|
154
|
+
* targets no existing account. A credential they lose still fails the
|
|
155
|
+
* round-trip on its 401 check (`is_unauthenticated_refusal`).
|
|
156
|
+
*
|
|
157
|
+
* @param principal - the principal `pick_auth_principal` chose
|
|
158
|
+
* @param side_effects - whether the request can change state
|
|
159
|
+
*/
|
|
160
|
+
export declare const round_trip_uses_fresh_account: (principal: AuthPrincipal, side_effects: boolean) => boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Whether a response says the request carried no valid credential: a 401
|
|
163
|
+
* whose reason is `authentication_required`, or that names no reason. A 401
|
|
164
|
+
* naming another reason is a handler's own answer to an authenticated call —
|
|
165
|
+
* `invalid_credentials` for a wrong current password — and is not this.
|
|
166
|
+
*
|
|
167
|
+
* @param status - the HTTP status
|
|
168
|
+
* @param reason - the refusal's reason: a REST body's `error`, a JSON-RPC
|
|
169
|
+
* error's `data.reason`
|
|
170
|
+
*/
|
|
171
|
+
export declare const is_unauthenticated_refusal: (status: number, reason: unknown) => boolean;
|
|
113
172
|
//# sourceMappingURL=integration_helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"integration_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration_helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@ import './assert_dev_env.js';
|
|
|
7
7
|
import { assert } from 'vitest';
|
|
8
8
|
import { is_null_schema, merge_error_schemas } from '../http/schema_helpers.js';
|
|
9
9
|
import { is_public_auth } from '../http/auth_shape.js';
|
|
10
|
+
import { ERROR_AUTHENTICATION_REQUIRED } from '../http/error_schemas.js';
|
|
10
11
|
import { create_session_cookie_value } from '../auth/session_cookie.js';
|
|
11
12
|
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
|
12
13
|
/**
|
|
@@ -245,28 +246,84 @@ export const assert_no_sensitive_fields_in_json = (body, blocklist, context) =>
|
|
|
245
246
|
}
|
|
246
247
|
};
|
|
247
248
|
/**
|
|
248
|
-
* Pick request
|
|
249
|
+
* Pick the principal a generated request calls as, from its declared auth.
|
|
250
|
+
* Shared by the REST and RPC round-trips (`pick_auth_headers`,
|
|
251
|
+
* `pick_rpc_auth`), so both call each auth shape as the same principal.
|
|
249
252
|
*
|
|
250
|
-
*
|
|
253
|
+
* Every principal but `'public'` carries a live credential of the kind the
|
|
254
|
+
* auth names, so the backend must authenticate it; a role mismatch (a keeper
|
|
255
|
+
* session on a role the keeper lacks) still refuses as 403, after
|
|
256
|
+
* authentication.
|
|
257
|
+
*/
|
|
258
|
+
export const pick_auth_principal = (auth) => {
|
|
259
|
+
if (is_public_auth(auth))
|
|
260
|
+
return 'public';
|
|
261
|
+
if (auth.credential_types?.includes('daemon_token'))
|
|
262
|
+
return 'daemon_token';
|
|
263
|
+
if (auth.roles?.length)
|
|
264
|
+
return auth.roles.includes(ROLE_ADMIN) ? 'admin' : 'keeper';
|
|
265
|
+
return 'authed';
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* The request headers carrying a principal's credential.
|
|
269
|
+
*
|
|
270
|
+
* @param principal - the principal `pick_auth_principal` chose
|
|
271
|
+
* @param keeper - the keeper's header builders
|
|
272
|
+
* @param authed_account - the account the `'authed'` principal calls as
|
|
273
|
+
* @param admin_account - the account the `'admin'` principal calls as
|
|
274
|
+
*/
|
|
275
|
+
export const to_principal_headers = (principal, keeper, authed_account, admin_account) => {
|
|
276
|
+
switch (principal) {
|
|
277
|
+
case 'public':
|
|
278
|
+
return { host: 'localhost', origin: 'http://localhost:5173' };
|
|
279
|
+
case 'daemon_token':
|
|
280
|
+
return keeper.create_daemon_token_headers();
|
|
281
|
+
case 'admin':
|
|
282
|
+
return admin_account.create_session_headers();
|
|
283
|
+
case 'keeper':
|
|
284
|
+
return keeper.create_session_headers();
|
|
285
|
+
case 'authed':
|
|
286
|
+
return authed_account.create_session_headers();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Pick request headers matching a route spec's auth requirement
|
|
291
|
+
* (`pick_auth_principal`, then `to_principal_headers`):
|
|
251
292
|
* - `none` — origin headers only
|
|
252
293
|
* - `authenticated` — the authed account's session cookie
|
|
253
294
|
* - `role: admin` — the admin account's session cookie
|
|
254
295
|
* - `role: <other>` — the keeper provider's session
|
|
255
296
|
* - `keeper` — the keeper provider's daemon token
|
|
256
297
|
*/
|
|
257
|
-
export const pick_auth_headers = (spec, keeper, authed_account, admin_account) =>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
298
|
+
export const pick_auth_headers = (spec, keeper, authed_account, admin_account) => to_principal_headers(pick_auth_principal(spec.auth), keeper, authed_account, admin_account);
|
|
299
|
+
/**
|
|
300
|
+
* Whether a round-trip sends a request as a fresh account rather than the
|
|
301
|
+
* shared authed one: a side-effecting request picked for the `'authed'`
|
|
302
|
+
* principal. Generated input omits optional fields, and an omitted target on
|
|
303
|
+
* a self-service action is the caller — `account_delete` deletes it,
|
|
304
|
+
* `account_session_revoke_all`, a logout or a password change end its
|
|
305
|
+
* session — so any side-effecting call may destroy the credential it was sent
|
|
306
|
+
* with, and on a shared account every later call would refuse as
|
|
307
|
+
* unauthenticated. Keyed on the request's own side-effect signal (an action's
|
|
308
|
+
* `side_effects`, a route's non-`GET` method) rather than a list of known
|
|
309
|
+
* destroyers, so a consumer's self-targeting action is covered without
|
|
310
|
+
* registering it. The other principals stay shared: the keeper and its daemon
|
|
311
|
+
* token are one per fixture, and an admin-gated request with generated nil ids
|
|
312
|
+
* targets no existing account. A credential they lose still fails the
|
|
313
|
+
* round-trip on its 401 check (`is_unauthenticated_refusal`).
|
|
314
|
+
*
|
|
315
|
+
* @param principal - the principal `pick_auth_principal` chose
|
|
316
|
+
* @param side_effects - whether the request can change state
|
|
317
|
+
*/
|
|
318
|
+
export const round_trip_uses_fresh_account = (principal, side_effects) => principal === 'authed' && side_effects;
|
|
319
|
+
/**
|
|
320
|
+
* Whether a response says the request carried no valid credential: a 401
|
|
321
|
+
* whose reason is `authentication_required`, or that names no reason. A 401
|
|
322
|
+
* naming another reason is a handler's own answer to an authenticated call —
|
|
323
|
+
* `invalid_credentials` for a wrong current password — and is not this.
|
|
324
|
+
*
|
|
325
|
+
* @param status - the HTTP status
|
|
326
|
+
* @param reason - the refusal's reason: a REST body's `error`, a JSON-RPC
|
|
327
|
+
* error's `data.reason`
|
|
328
|
+
*/
|
|
329
|
+
export const is_unauthenticated_refusal = (status, reason) => status === 401 && (reason === undefined || reason === ERROR_AUTHENTICATION_REQUIRED);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
|
+
import { type AuthPrincipal } from './integration_helpers.js';
|
|
2
3
|
import type { SetupTest, TestFixture } from './cross_backend/setup.js';
|
|
3
4
|
import type { AppSurfaceSpec } from '../http/surface.js';
|
|
4
5
|
/** Options for `describe_round_trip_validation`. */
|
|
@@ -38,6 +39,22 @@ export interface RoundTripTestOptions {
|
|
|
38
39
|
body?: Record<string, unknown>;
|
|
39
40
|
}>>;
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Guard against a refusal that says the request carried no valid credential
|
|
44
|
+
* when it carried one (`is_unauthenticated_refusal`), the REST twin of
|
|
45
|
+
* `assert_rpc_authenticated`. Every non-public route is sent a live
|
|
46
|
+
* credential its auth admits, so a 401 reading `authentication_required`, or
|
|
47
|
+
* naming no reason, means the credential stopped working — most often because
|
|
48
|
+
* an earlier request ended it. A public route may answer 401 (a login with a
|
|
49
|
+
* wrong password), and so may a handler refusing an authenticated request
|
|
50
|
+
* with a reason of its own (`invalid_credentials`).
|
|
51
|
+
*
|
|
52
|
+
* @param route_key - the route, as `METHOD /path`
|
|
53
|
+
* @param status - the HTTP status
|
|
54
|
+
* @param body - the parsed response body, or `undefined` when not JSON
|
|
55
|
+
* @param principal - the principal the request was sent as
|
|
56
|
+
*/
|
|
57
|
+
export declare const assert_route_authenticated: (route_key: string, status: number, body: unknown, principal: AuthPrincipal) => void;
|
|
41
58
|
/**
|
|
42
59
|
* Run schema-driven round-trip validation tests.
|
|
43
60
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA6B7B,OAAO,EAON,KAAK,aAAa,EAClB,MAAM,0BAA0B,CAAC;AAMlC,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,oDAAoD;AACpD,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5B,+EAA+E;IAC/E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACvD;;;;;;;;;;;;;OAaG;IACH,gBAAgB,CAAC,EAAE,GAAG,CACrB,MAAM,EACN,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CACnF,CAAC;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,0BAA0B,GACtC,WAAW,MAAM,EACjB,QAAQ,MAAM,EACd,MAAM,OAAO,EACb,WAAW,aAAa,KACtB,IAUF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,8BAA8B,GAAI,SAAS,oBAAoB,KAAG,IAmJ9E,CAAC"}
|
|
@@ -8,19 +8,48 @@ import './assert_dev_env.js';
|
|
|
8
8
|
* `setup_test` fixture-producing callback.
|
|
9
9
|
*
|
|
10
10
|
* Cadence: per-describe `setup_test()` call. Each test in the suite
|
|
11
|
-
* fires one HTTP request against a shared fixture
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* fires one HTTP request against a shared fixture, so the per-test cost of
|
|
12
|
+
* re-bootstrapping isn't paid here. A mutation called as the authed account
|
|
13
|
+
* runs on a fresh account of its own (`round_trip_uses_fresh_account`) — a
|
|
14
|
+
* generated logout or self-delete would otherwise end the shared account's
|
|
15
|
+
* credential — and a 401 to any non-public request fails
|
|
16
|
+
* (`is_unauthenticated_refusal`), so a credential lost some other way goes
|
|
17
|
+
* red rather than vacuous. Suites with state-isolation requirements
|
|
18
|
+
* (integration, admin_integration, audit_completeness) call `setup_test()`
|
|
19
|
+
* per test.
|
|
16
20
|
*
|
|
17
21
|
* @module
|
|
18
22
|
*/
|
|
19
23
|
import { describe, test, beforeAll, assert } from 'vitest';
|
|
20
24
|
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
|
21
|
-
import {
|
|
22
|
-
import { assert_response_matches_spec, pick_auth_headers } from './integration_helpers.js';
|
|
25
|
+
import { needs_actor, input_schema_declares_acting } from '../http/auth_shape.js';
|
|
26
|
+
import { assert_response_matches_spec, is_unauthenticated_refusal, pick_auth_headers, pick_auth_principal, round_trip_uses_fresh_account, to_principal_headers } from './integration_helpers.js';
|
|
23
27
|
import { resolve_valid_path, generate_valid_body, to_route_content_type } from './schema_generators.js';
|
|
28
|
+
/**
|
|
29
|
+
* Guard against a refusal that says the request carried no valid credential
|
|
30
|
+
* when it carried one (`is_unauthenticated_refusal`), the REST twin of
|
|
31
|
+
* `assert_rpc_authenticated`. Every non-public route is sent a live
|
|
32
|
+
* credential its auth admits, so a 401 reading `authentication_required`, or
|
|
33
|
+
* naming no reason, means the credential stopped working — most often because
|
|
34
|
+
* an earlier request ended it. A public route may answer 401 (a login with a
|
|
35
|
+
* wrong password), and so may a handler refusing an authenticated request
|
|
36
|
+
* with a reason of its own (`invalid_credentials`).
|
|
37
|
+
*
|
|
38
|
+
* @param route_key - the route, as `METHOD /path`
|
|
39
|
+
* @param status - the HTTP status
|
|
40
|
+
* @param body - the parsed response body, or `undefined` when not JSON
|
|
41
|
+
* @param principal - the principal the request was sent as
|
|
42
|
+
*/
|
|
43
|
+
export const assert_route_authenticated = (route_key, status, body, principal) => {
|
|
44
|
+
if (principal === 'public')
|
|
45
|
+
return;
|
|
46
|
+
const reason = body !== null && typeof body === 'object' ? body.error : undefined;
|
|
47
|
+
if (!is_unauthenticated_refusal(status, reason))
|
|
48
|
+
return;
|
|
49
|
+
assert.fail(`${route_key} answered 401 (${JSON.stringify(reason ?? null)}) to the ${principal}` +
|
|
50
|
+
` principal's credential — the request carried a credential its auth admits,` +
|
|
51
|
+
` so a 401 here means that credential stopped working`);
|
|
52
|
+
};
|
|
24
53
|
/**
|
|
25
54
|
* Run schema-driven round-trip validation tests.
|
|
26
55
|
*
|
|
@@ -51,22 +80,23 @@ export const describe_round_trip_validation = (options) => {
|
|
|
51
80
|
roles: [ROLE_ADMIN]
|
|
52
81
|
});
|
|
53
82
|
});
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
// The actor id of whichever account the principal calls as — so an
|
|
84
|
+
// `actor: 'required'` route that declares `acting?: ActingActor` gets the
|
|
85
|
+
// matching actor supplied explicitly (its sole actor, here), rather than
|
|
86
|
+
// relying on implicit single-actor resolution. Keeps such routes drivable
|
|
87
|
+
// without a consumer skip-list entry. `null` for public routes.
|
|
88
|
+
const to_acting_actor_id = (principal, authed) => {
|
|
89
|
+
switch (principal) {
|
|
90
|
+
case 'public':
|
|
91
|
+
return null;
|
|
92
|
+
case 'admin':
|
|
93
|
+
return admin_account.actor.id;
|
|
94
|
+
case 'authed':
|
|
95
|
+
return authed.actor.id;
|
|
96
|
+
case 'daemon_token':
|
|
97
|
+
case 'keeper':
|
|
98
|
+
return fixture.actor.id;
|
|
68
99
|
}
|
|
69
|
-
return authed_account.actor.id;
|
|
70
100
|
};
|
|
71
101
|
test.each(describe_time_specs)('$method $path produces schema-valid response', async (spec) => {
|
|
72
102
|
const route_key = `${spec.method} ${spec.path}`;
|
|
@@ -81,13 +111,19 @@ export const describe_round_trip_validation = (options) => {
|
|
|
81
111
|
const url = resolve_valid_path(spec.path, spec.params);
|
|
82
112
|
const override = options.input_overrides?.get(route_key);
|
|
83
113
|
let body = override ?? generate_valid_body(spec.input);
|
|
84
|
-
|
|
114
|
+
// a mutation called as the authed account runs on a fresh one, so a
|
|
115
|
+
// logout or a self-delete can't end the shared account's credential
|
|
116
|
+
const principal = pick_auth_principal(spec.auth);
|
|
117
|
+
const authed = round_trip_uses_fresh_account(principal, spec.method !== 'GET')
|
|
118
|
+
? await fixture.create_account({ roles: [] })
|
|
119
|
+
: authed_account;
|
|
120
|
+
const headers = to_principal_headers(principal, fixture, authed, admin_account);
|
|
85
121
|
// Auto-supply `acting` for actor-required routes that declare it. The
|
|
86
122
|
// `actor !== 'none' ⟺ acting declared` registry invariant means a
|
|
87
123
|
// route either declares `acting` in `query` (REST GET/body-less) or
|
|
88
124
|
// `input` — supply the picked account's actor in the matching channel.
|
|
89
125
|
let request_url = url;
|
|
90
|
-
const acting_id = needs_actor(spec.auth) ?
|
|
126
|
+
const acting_id = needs_actor(spec.auth) ? to_acting_actor_id(principal, authed) : null;
|
|
91
127
|
if (acting_id !== null) {
|
|
92
128
|
if (spec.query && input_schema_declares_acting(spec.query)) {
|
|
93
129
|
request_url = `${url}${url.includes('?') ? '&' : '?'}acting=${acting_id}`;
|
|
@@ -107,6 +143,13 @@ export const describe_round_trip_validation = (options) => {
|
|
|
107
143
|
return;
|
|
108
144
|
}
|
|
109
145
|
try {
|
|
146
|
+
if (res.status === 401) {
|
|
147
|
+
const body = await res
|
|
148
|
+
.clone()
|
|
149
|
+
.json()
|
|
150
|
+
.catch(() => undefined);
|
|
151
|
+
assert_route_authenticated(route_key, res.status, body, principal);
|
|
152
|
+
}
|
|
110
153
|
await assert_response_matches_spec(describe_time_specs, spec.method, url, res);
|
|
111
154
|
}
|
|
112
155
|
catch (e) {
|
|
@@ -6,6 +6,16 @@ export interface RpcAttackSurfaceOptions {
|
|
|
6
6
|
build: () => AppSurfaceSpec;
|
|
7
7
|
/** All roles in the app (e.g. `['admin', 'keeper']`). */
|
|
8
8
|
roles: Array<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Valid params per method (method name → params) to build its adversarial
|
|
11
|
+
* params cases from, in place of the generated base — for an input whose
|
|
12
|
+
* valid shape the generator can't reach. Each must pass the method's input
|
|
13
|
+
* schema; one that doesn't throws at collection, naming the method. The
|
|
14
|
+
* cases are sent as a single-actor principal, so an override omits
|
|
15
|
+
* `acting`. The cross-process twin is
|
|
16
|
+
* `RpcAdversarialCrossTestOptions.input_overrides`.
|
|
17
|
+
*/
|
|
18
|
+
input_overrides?: ReadonlyMap<string, Record<string, unknown>>;
|
|
9
19
|
}
|
|
10
20
|
/**
|
|
11
21
|
* Run the standard RPC attack surface test suite.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc_attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAkB7B,OAAO,KAAK,EAGX,cAAc,EACd,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"rpc_attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAkB7B,OAAO,KAAK,EAGX,cAAc,EACd,MAAM,oBAAoB,CAAC;AA+B5B,uDAAuD;AACvD,MAAM,WAAW,uBAAuB;IACvC,+FAA+F;IAC/F,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC/D;AA2VD;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,uBAAuB,KAAG,IAOpF,CAAC"}
|
|
@@ -16,10 +16,10 @@ import { test, assert, describe } from 'vitest';
|
|
|
16
16
|
import { is_keeper_auth, is_public_auth, is_role_auth } from '../http/auth_shape.js';
|
|
17
17
|
import { JSONRPC_ERROR_CODES } from '../http/jsonrpc_errors.js';
|
|
18
18
|
import { create_auth_test_apps, create_test_app_from_specs, create_test_request_context, select_auth_app } from './auth_apps.js';
|
|
19
|
-
import { generate_input_test_cases } from './adversarial_input.js';
|
|
20
19
|
import { generate_valid_body } from './schema_generators.js';
|
|
21
|
-
import { ERROR_INVALID_JSON_BODY } from '../http/error_schemas.js';
|
|
22
20
|
import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response } from './rpc_helpers.js';
|
|
21
|
+
import { assert_rpc_envelope_refusal, create_rpc_envelope_cases, generate_rpc_params_test_cases, to_rpc_envelope_request } from './rpc_envelope_cases.js';
|
|
22
|
+
import { expected_invalid_reason } from './adversarial_input_cases.js';
|
|
23
23
|
// --- Helpers ---
|
|
24
24
|
/** Filter RPC methods that require any form of authentication. */
|
|
25
25
|
const filter_protected_rpc_methods = (endpoint) => endpoint.methods.filter((m) => !is_public_auth(m.auth));
|
|
@@ -169,148 +169,27 @@ const describe_rpc_auth = (options) => {
|
|
|
169
169
|
/**
|
|
170
170
|
* Generate adversarial envelope tests for RPC endpoints.
|
|
171
171
|
*
|
|
172
|
-
*
|
|
173
|
-
* envelope parsing
|
|
172
|
+
* Runs `create_rpc_envelope_cases` for each endpoint: the dispatcher's
|
|
173
|
+
* envelope parsing, method lookup, and GET read restriction.
|
|
174
174
|
*/
|
|
175
175
|
const describe_rpc_adversarial_envelopes = (options) => {
|
|
176
176
|
const { build, roles } = options;
|
|
177
177
|
const { surface, route_specs } = build();
|
|
178
178
|
if (surface.rpc_endpoints.length === 0)
|
|
179
179
|
return;
|
|
180
|
-
|
|
181
|
-
const apps = create_auth_test_apps(route_specs, []);
|
|
182
|
-
// authed apps for the GET mutation test (needs correct auth to reach the side_effects check)
|
|
180
|
+
const public_apps = create_auth_test_apps(route_specs, []);
|
|
183
181
|
const authed_apps = create_auth_test_apps(route_specs, roles);
|
|
184
182
|
describe('RPC adversarial envelopes', () => {
|
|
185
183
|
for (const endpoint of surface.rpc_endpoints) {
|
|
186
|
-
// find a mutation method for GET-restriction testing
|
|
187
|
-
const mutation_method = endpoint.methods.find((m) => m.side_effects);
|
|
188
184
|
describe(endpoint.path, () => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const body = await res.json();
|
|
198
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.parse_error);
|
|
199
|
-
});
|
|
200
|
-
test('wrong jsonrpc version → invalid_request', async () => {
|
|
201
|
-
const res = await apps.public.request(endpoint.path, {
|
|
202
|
-
method: 'POST',
|
|
203
|
-
headers: { 'Content-Type': 'application/json' },
|
|
204
|
-
body: JSON.stringify({
|
|
205
|
-
jsonrpc: '1.0',
|
|
206
|
-
id: 'test',
|
|
207
|
-
method: endpoint.methods[0]?.name ?? 'any'
|
|
208
|
-
})
|
|
209
|
-
});
|
|
210
|
-
assert.strictEqual(res.status, 400);
|
|
211
|
-
const body = await res.json();
|
|
212
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
213
|
-
});
|
|
214
|
-
test('missing jsonrpc field → invalid_request', async () => {
|
|
215
|
-
const res = await apps.public.request(endpoint.path, {
|
|
216
|
-
method: 'POST',
|
|
217
|
-
headers: { 'Content-Type': 'application/json' },
|
|
218
|
-
body: JSON.stringify({
|
|
219
|
-
id: 'test',
|
|
220
|
-
method: endpoint.methods[0]?.name ?? 'any'
|
|
221
|
-
})
|
|
222
|
-
});
|
|
223
|
-
assert.strictEqual(res.status, 400);
|
|
224
|
-
const body = await res.json();
|
|
225
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
226
|
-
});
|
|
227
|
-
test('missing method field → invalid_request', async () => {
|
|
228
|
-
const res = await apps.public.request(endpoint.path, {
|
|
229
|
-
method: 'POST',
|
|
230
|
-
headers: { 'Content-Type': 'application/json' },
|
|
231
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: 'test' })
|
|
232
|
-
});
|
|
233
|
-
assert.strictEqual(res.status, 400);
|
|
234
|
-
const body = await res.json();
|
|
235
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
236
|
-
});
|
|
237
|
-
test('missing id field → invalid_request', async () => {
|
|
238
|
-
const res = await apps.public.request(endpoint.path, {
|
|
239
|
-
method: 'POST',
|
|
240
|
-
headers: { 'Content-Type': 'application/json' },
|
|
241
|
-
body: JSON.stringify({
|
|
242
|
-
jsonrpc: '2.0',
|
|
243
|
-
method: endpoint.methods[0]?.name ?? 'any'
|
|
244
|
-
})
|
|
245
|
-
});
|
|
246
|
-
assert.strictEqual(res.status, 400);
|
|
247
|
-
const body = await res.json();
|
|
248
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
249
|
-
});
|
|
250
|
-
test('batch (array) body → invalid_request', async () => {
|
|
251
|
-
const res = await apps.public.request(endpoint.path, {
|
|
252
|
-
method: 'POST',
|
|
253
|
-
headers: { 'Content-Type': 'application/json' },
|
|
254
|
-
body: JSON.stringify([
|
|
255
|
-
{ jsonrpc: '2.0', id: '1', method: endpoint.methods[0]?.name ?? 'any' }
|
|
256
|
-
])
|
|
257
|
-
});
|
|
258
|
-
assert.strictEqual(res.status, 400);
|
|
259
|
-
const body = await res.json();
|
|
260
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
261
|
-
assert.strictEqual(body.id, null, 'batch has no extractable id');
|
|
262
|
-
});
|
|
263
|
-
test('unknown method name → method_not_found', async () => {
|
|
264
|
-
const res = await apps.public.request(endpoint.path, create_rpc_post_init('__nonexistent_method__'));
|
|
265
|
-
assert.strictEqual(res.status, 404);
|
|
266
|
-
const body = await res.json();
|
|
267
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.method_not_found);
|
|
268
|
-
});
|
|
269
|
-
// --- GET envelope malformation ---
|
|
270
|
-
test('GET missing method → invalid_request', async () => {
|
|
271
|
-
const res = await apps.public.request(`${endpoint.path}?id=test`);
|
|
272
|
-
assert.strictEqual(res.status, 400);
|
|
273
|
-
const body = await res.json();
|
|
274
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
275
|
-
});
|
|
276
|
-
test('GET missing id → invalid_request', async () => {
|
|
277
|
-
const first_method = endpoint.methods[0]?.name ?? 'any';
|
|
278
|
-
const res = await apps.public.request(`${endpoint.path}?method=${first_method}`);
|
|
279
|
-
assert.strictEqual(res.status, 400);
|
|
280
|
-
const body = await res.json();
|
|
281
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
282
|
-
});
|
|
283
|
-
test('GET invalid JSON params → invalid_params', async () => {
|
|
284
|
-
const read_method = endpoint.methods.find((m) => !m.side_effects);
|
|
285
|
-
// skip if no read methods exist
|
|
286
|
-
if (!read_method)
|
|
287
|
-
return;
|
|
288
|
-
const res = await apps.public.request(`${endpoint.path}?method=${read_method.name}&id=test¶ms=not-json`);
|
|
289
|
-
assert.strictEqual(res.status, 400);
|
|
290
|
-
const body = await res.json();
|
|
291
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_params);
|
|
292
|
-
});
|
|
293
|
-
test('GET non-object params → error', async () => {
|
|
294
|
-
const read_method = endpoint.methods.find((m) => !m.side_effects);
|
|
295
|
-
// skip if no read methods exist
|
|
296
|
-
if (!read_method)
|
|
297
|
-
return;
|
|
298
|
-
// valid JSON but not an object — hits dispatcher's params validation
|
|
299
|
-
const res = await apps.public.request(`${endpoint.path}?method=${read_method.name}&id=test¶ms=42`);
|
|
300
|
-
// should reject: either invalid_params (validation) or auth error
|
|
301
|
-
assert.ok(res.status >= 400, `expected error status for non-object params, got ${res.status}`);
|
|
302
|
-
const body = await res.json();
|
|
303
|
-
assert_jsonrpc_error_response(body);
|
|
304
|
-
});
|
|
305
|
-
if (mutation_method) {
|
|
306
|
-
test('GET mutation method → invalid_request (side effects)', async () => {
|
|
307
|
-
const url = create_rpc_get_url(endpoint.path, mutation_method.name);
|
|
308
|
-
// need correct auth to reach the side_effects check
|
|
309
|
-
const app = select_auth_app(authed_apps, mutation_method.auth);
|
|
310
|
-
const res = await app.request(url);
|
|
311
|
-
assert.strictEqual(res.status, 400);
|
|
312
|
-
const body = await res.json();
|
|
313
|
-
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
185
|
+
for (const c of create_rpc_envelope_cases(endpoint)) {
|
|
186
|
+
test(c.name, async () => {
|
|
187
|
+
const app = c.as === 'anonymous'
|
|
188
|
+
? public_apps.public
|
|
189
|
+
: select_auth_app(authed_apps, c.method.auth);
|
|
190
|
+
const { url, init } = to_rpc_envelope_request(endpoint.path, c);
|
|
191
|
+
const res = await app.request(url, init);
|
|
192
|
+
assert_rpc_envelope_refusal(res.status, await res.json(), c);
|
|
314
193
|
});
|
|
315
194
|
}
|
|
316
195
|
});
|
|
@@ -323,8 +202,10 @@ const describe_rpc_adversarial_envelopes = (options) => {
|
|
|
323
202
|
*
|
|
324
203
|
* For each method with a non-null input schema, generates test cases
|
|
325
204
|
* from the schema (wrong types, missing fields, format violations)
|
|
326
|
-
* and wraps them in valid JSON-RPC envelopes
|
|
327
|
-
* `
|
|
205
|
+
* and wraps them in valid JSON-RPC envelopes, via `generate_rpc_params_test_cases`;
|
|
206
|
+
* a method's `input_overrides` entry, when given, is the base they're built from.
|
|
207
|
+
* Each owes 400 `invalid_params` with the broken field's declared
|
|
208
|
+
* `invalid_reason` as `data.reason`, or no reason (`expected_invalid_reason`).
|
|
328
209
|
*/
|
|
329
210
|
const describe_rpc_adversarial_params = (options) => {
|
|
330
211
|
const { build, roles } = options;
|
|
@@ -348,10 +229,10 @@ const describe_rpc_adversarial_params = (options) => {
|
|
|
348
229
|
});
|
|
349
230
|
continue;
|
|
350
231
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
232
|
+
const test_cases = generate_rpc_params_test_cases(action.spec.input, {
|
|
233
|
+
base: options.input_overrides?.get(method.name),
|
|
234
|
+
label: method.name
|
|
235
|
+
});
|
|
355
236
|
if (test_cases.length === 0)
|
|
356
237
|
continue;
|
|
357
238
|
total_cases += test_cases.length;
|
|
@@ -363,6 +244,12 @@ const describe_rpc_adversarial_params = (options) => {
|
|
|
363
244
|
assert.strictEqual(res.status, 400, `Expected 400 for ${method.name} [${tc.label}], got ${res.status}`);
|
|
364
245
|
const body = await res.json();
|
|
365
246
|
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_params);
|
|
247
|
+
// the broken field's declared reason, or none
|
|
248
|
+
const data = body.error.data;
|
|
249
|
+
const reason = typeof data === 'object' && data !== null
|
|
250
|
+
? data.reason
|
|
251
|
+
: undefined;
|
|
252
|
+
assert.strictEqual(reason, expected_invalid_reason(action.spec.input, tc), `data.reason for ${method.name} [${tc.label}]`);
|
|
366
253
|
});
|
|
367
254
|
}
|
|
368
255
|
});
|