@fuzdev/fuz_app 0.140.0 → 0.142.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 +72 -22
- package/dist/actions/action_rpc.d.ts +5 -1
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +9 -1
- package/dist/actions/connection_closer.d.ts +44 -4
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +33 -3
- package/dist/actions/perform_action.d.ts +10 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +23 -9
- package/dist/actions/register_action_ws.d.ts +42 -6
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +87 -19
- package/dist/actions/register_ws_endpoint.d.ts +15 -6
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +10 -4
- package/dist/actions/socket.svelte.d.ts +19 -4
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +31 -4
- package/dist/actions/transports.d.ts +30 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +30 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +24 -10
- package/dist/actions/transports_ws_backend.d.ts +51 -5
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +62 -9
- package/dist/actions/ws_endpoint_spec.d.ts +11 -10
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +44 -22
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +12 -1
- package/dist/auth/account_route_schema.d.ts +1 -0
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +12 -0
- package/dist/auth/account_routes.d.ts +7 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +240 -187
- package/dist/auth/admin_action_specs.d.ts +6 -1
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +6 -1
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +14 -2
- package/dist/auth/api_token.d.ts +20 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +21 -1
- package/dist/auth/api_token_queries.d.ts +22 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +11 -6
- package/dist/auth/audit_log_routes.d.ts +10 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +20 -0
- package/dist/auth/audit_log_schema.d.ts +30 -5
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +30 -0
- package/dist/auth/bearer_auth.d.ts +4 -2
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +19 -8
- package/dist/auth/bootstrap_route_schema.d.ts +1 -0
- package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +85 -76
- package/dist/auth/cell_action_specs.d.ts +7 -3
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +9 -5
- package/dist/auth/cell_actions.d.ts +3 -3
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +67 -38
- package/dist/auth/cell_authorize.d.ts +69 -8
- package/dist/auth/cell_authorize.d.ts.map +1 -1
- package/dist/auth/cell_authorize.js +89 -9
- package/dist/auth/cell_field_action_specs.d.ts +5 -4
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +7 -7
- package/dist/auth/cell_field_actions.d.ts +7 -5
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +24 -22
- package/dist/auth/cell_grant_action_specs.d.ts +6 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_grant_action_specs.js +6 -0
- package/dist/auth/cell_grant_actions.d.ts +3 -1
- package/dist/auth/cell_grant_actions.d.ts.map +1 -1
- package/dist/auth/cell_grant_actions.js +16 -2
- package/dist/auth/cell_item_action_specs.d.ts +5 -4
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +7 -7
- package/dist/auth/cell_item_actions.d.ts +6 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +23 -22
- package/dist/auth/cell_relation_list_limit.d.ts +20 -0
- package/dist/auth/cell_relation_list_limit.d.ts.map +1 -0
- package/dist/auth/cell_relation_list_limit.js +27 -0
- package/dist/auth/cell_relation_visibility.d.ts +4 -3
- package/dist/auth/cell_relation_visibility.d.ts.map +1 -1
- package/dist/auth/cell_relation_visibility.js +6 -5
- package/dist/auth/cell_steward.d.ts +45 -0
- package/dist/auth/cell_steward.d.ts.map +1 -0
- package/dist/auth/cell_steward.js +53 -0
- package/dist/auth/cleanup.d.ts +9 -9
- package/dist/auth/cleanup.js +9 -9
- package/dist/auth/credential_deadline.d.ts +95 -0
- package/dist/auth/credential_deadline.d.ts.map +1 -0
- package/dist/auth/credential_deadline.js +139 -0
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +4 -1
- package/dist/auth/middleware.d.ts +90 -12
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +115 -16
- package/dist/auth/request_context.d.ts +8 -2
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +20 -4
- package/dist/auth/resolved_auth.d.ts +13 -2
- package/dist/auth/resolved_auth.d.ts.map +1 -1
- package/dist/auth/resolved_auth.js +5 -4
- package/dist/auth/role_grant_offer_action_specs.d.ts +5 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +6 -1
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +23 -16
- package/dist/auth/role_grant_offer_queries.d.ts +8 -6
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +16 -8
- package/dist/auth/session_cookie.d.ts +4 -0
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +3 -2
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +4 -1
- package/dist/auth/session_queries.d.ts +19 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +8 -2
- package/dist/auth/signup_route_schema.d.ts.map +1 -1
- package/dist/auth/signup_route_schema.js +1 -0
- package/dist/auth/signup_routes.d.ts +4 -3
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +157 -134
- package/dist/auth/token_scope.d.ts +7 -1
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +8 -2
- package/dist/db/CLAUDE.md +32 -19
- package/dist/db/cell_ddl.d.ts +1 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +1 -2
- package/dist/db/cell_field_queries.d.ts +20 -18
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +28 -22
- package/dist/db/cell_item_queries.d.ts +20 -14
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +28 -18
- package/dist/db/cell_moderation.d.ts +67 -0
- package/dist/db/cell_moderation.d.ts.map +1 -0
- package/dist/db/cell_moderation.js +66 -0
- package/dist/db/cell_queries.d.ts +44 -14
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +68 -45
- package/dist/db/cell_view_sql.d.ts +102 -0
- package/dist/db/cell_view_sql.d.ts.map +1 -0
- package/dist/db/cell_view_sql.js +112 -0
- package/dist/db/db_pg.d.ts +1 -1
- package/dist/db/db_pg.js +1 -1
- package/dist/hono_context.d.ts +23 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +19 -0
- package/dist/http/CLAUDE.md +41 -15
- package/dist/http/auth_shape.d.ts +5 -2
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +5 -2
- package/dist/http/ip_canonical.d.ts +30 -3
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +50 -3
- package/dist/http/middleware_spec.d.ts +16 -0
- package/dist/http/middleware_spec.d.ts.map +1 -1
- package/dist/http/origin.d.ts +113 -18
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +247 -127
- package/dist/http/route_spec.d.ts +21 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/schema_helpers.d.ts +22 -0
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +24 -0
- package/dist/http/surface.d.ts +6 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +12 -5
- package/dist/primitive_schemas.d.ts +10 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +11 -2
- package/dist/rate_limiter.d.ts +179 -12
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +352 -42
- package/dist/realtime/sse_auth_guard.d.ts +22 -6
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +31 -12
- package/dist/realtime/subscriber_registry.d.ts +10 -1
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +10 -1
- package/dist/server/app_server.d.ts +20 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +8 -3
- package/dist/server/env.d.ts +2 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +7 -17
- package/dist/server/serve_fact_route.d.ts +8 -4
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +16 -6
- package/dist/testing/CLAUDE.md +164 -25
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -6
- package/dist/testing/connection_closer_helpers.d.ts +6 -1
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +4 -0
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +37 -2
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +14 -65
- package/dist/testing/cross_backend/capabilities.d.ts +26 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +9 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts +38 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +401 -5
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +145 -2
- package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +6 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +20 -19
- package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
- package/dist/testing/cross_backend/credential_deadline.js +185 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +13 -2
- package/dist/testing/cross_backend/expected_schema.json +0 -1
- package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +7 -1
- package/dist/testing/cross_backend/login_security.d.ts +16 -0
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +154 -0
- package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
- package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
- package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +6 -26
- package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
- package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_conformance.js +136 -0
- package/dist/testing/cross_backend/origin_conformance.json +1679 -0
- package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_scope.js +140 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +24 -2
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +64 -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 +6 -4
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +20 -1
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +23 -1
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +20 -7
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +25 -21
- package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
- package/dist/testing/cross_backend/transport_composition.js +364 -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 +21 -6
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +21 -1
- package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_role_gated.js +142 -0
- package/dist/testing/entities.d.ts +23 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +37 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +3 -1
- package/dist/testing/middleware.d.ts +10 -0
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +31 -7
- package/dist/testing/schema_introspect.js +1 -1
- package/dist/testing/schema_parity.d.ts +4 -3
- package/dist/testing/schema_parity.d.ts.map +1 -1
- package/dist/testing/schema_parity.js +4 -3
- package/dist/testing/schema_ready_fixture.d.ts +2 -4
- package/dist/testing/schema_ready_fixture.d.ts.map +1 -1
- package/dist/testing/schema_ready_fixture.js +2 -4
- package/dist/testing/stubs.d.ts +16 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +20 -7
- package/dist/testing/surface_invariants.js +2 -2
- package/dist/ui/auth_state.svelte.d.ts +10 -0
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +22 -4
- package/package.json +1 -1
- package/dist/db/cell_history_ddl.d.ts +0 -38
- package/dist/db/cell_history_ddl.d.ts.map +0 -1
- package/dist/db/cell_history_ddl.js +0 -61
|
@@ -5,7 +5,7 @@ import { create_standard_rpc_actions } from '../../auth/standard_rpc_actions.js'
|
|
|
5
5
|
import { create_all_cell_actions } from '../../auth/all_cell_actions.js';
|
|
6
6
|
import { create_actor_lookup_actions } from '../../auth/actor_lookup_actions.js';
|
|
7
7
|
import { create_actor_search_actions } from '../../auth/actor_search_actions.js';
|
|
8
|
-
import { create_testing_action_manifest_action, create_testing_actions } from './testing_reset_actions.js';
|
|
8
|
+
import { create_testing_action_manifest_action, create_testing_actions, create_testing_origin_check_action, create_testing_shorten_credential_expiry_action } from './testing_reset_actions.js';
|
|
9
9
|
import { test_cell_gated_create_authorize } from './test_cell_gated_create_authorize.js';
|
|
10
10
|
import { spine_roles, spine_session_options } from './default_spine_surface.js';
|
|
11
11
|
/**
|
|
@@ -39,6 +39,12 @@ export const build_full_spine_rpc_actions = (deps, options) => {
|
|
|
39
39
|
...create_all_cell_actions({ ...deps, authorize_create: test_cell_gated_create_authorize }, { roles: spine_roles }),
|
|
40
40
|
...create_actor_lookup_actions(deps),
|
|
41
41
|
...create_actor_search_actions(deps),
|
|
42
|
+
// the origin conformance table's probe — kept out of
|
|
43
|
+
// `create_testing_actions` (see its spec doc)
|
|
44
|
+
create_testing_origin_check_action(),
|
|
45
|
+
// the credential-deadline suite's shorten-only seam — kept out of
|
|
46
|
+
// `create_testing_actions` for the same reason
|
|
47
|
+
create_testing_shorten_credential_expiry_action(),
|
|
42
48
|
// `peer/ping` is mounted on the HTTP RPC endpoint too (not just WS) so an
|
|
43
49
|
// HTTP invocation reaches the handler and refuses with `peer_no_transport`
|
|
44
50
|
// rather than `method_not_found`. It's a protocol action, so it's filtered
|
|
@@ -6,6 +6,22 @@ export interface LoginSecurityCrossTestOptions {
|
|
|
6
6
|
readonly setup_test: SetupTest;
|
|
7
7
|
/** REST login route path. Default `/api/account/login` (the spine convention). */
|
|
8
8
|
readonly login_path?: string;
|
|
9
|
+
/** REST password-change route path. Default `/api/account/password`. */
|
|
10
|
+
readonly password_path?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the backend serves concurrent logins — each its own database
|
|
13
|
+
* transaction — at once, for the burst-of-correct-logins case. Default
|
|
14
|
+
* `false`: a backend on one in-process connection (pglet-wasm, PGlite)
|
|
15
|
+
* refuses a second top-level transaction while one runs, so concurrent
|
|
16
|
+
* successful logins answer `500` there whatever the limiter does.
|
|
17
|
+
*/
|
|
18
|
+
readonly concurrent_transactions?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The backend's base URL, for the repeated-`X-Forwarded-For` case, which
|
|
21
|
+
* needs a raw socket to send two header lines. `null` (the default) skips
|
|
22
|
+
* that case.
|
|
23
|
+
*/
|
|
24
|
+
readonly base_url?: string | null;
|
|
9
25
|
}
|
|
10
26
|
export declare const describe_login_security_cross_tests: (options: LoginSecurityCrossTestOptions) => void;
|
|
11
27
|
//# sourceMappingURL=login_security.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login_security.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/login_security.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"login_security.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/login_security.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAgF9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,mDAAmD;AACnD,MAAM,WAAW,6BAA6B;IAC7C,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAgDD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IA4UF,CAAC"}
|
|
@@ -24,6 +24,26 @@ import '../assert_dev_env.js';
|
|
|
24
24
|
* not `429`). A backend that ignored XFF and keyed on the (loopback) TCP
|
|
25
25
|
* peer would 429 the fresh-IP request too — so the `401` proves the limiter
|
|
26
26
|
* keys on the resolved `X-Forwarded-For` client IP.
|
|
27
|
+
* - **IPv6 clients share a bucket per `/64`** — after one forwarded IPv6
|
|
28
|
+
* address exhausts its bucket, another address in the same `/64` is `429`
|
|
29
|
+
* and one in the next `/64` is not; the login audit rows keep each full
|
|
30
|
+
* address, never the bucket key. A password change, which shares the
|
|
31
|
+
* per-IP login limiter, from an address in an exhausted `/64` is `429` too.
|
|
32
|
+
* - **a concurrent burst gets at most the cap past the limiter** — `2 ×
|
|
33
|
+
* ip_limit` wrong logins sent at once from one fresh forwarded IP answer at
|
|
34
|
+
* most `ip_limit` `401`s and `429` for the rest. A limiter checked before
|
|
35
|
+
* the lookup and recorded after the verify lets the whole burst through; each
|
|
36
|
+
* attempt is reserved instead, counted in flight from the check on.
|
|
37
|
+
* - **a burst of correct logins past the cap waits, then succeeds** — attempts
|
|
38
|
+
* count while in flight whatever their outcome, so `2 × ip_limit` correct
|
|
39
|
+
* logins at once from one address meet the cap; the ones past it wait for a
|
|
40
|
+
* slot (at most the cap of them) and all answer `200`. Runs on a backend
|
|
41
|
+
* that serves concurrent transactions (`concurrent_transactions`).
|
|
42
|
+
* - **every `X-Forwarded-For` line is read** — a client that sends its own
|
|
43
|
+
* line ahead of the one the trusted proxy appended can't choose its bucket:
|
|
44
|
+
* after exhausting one forwarded IP, a raw request carrying a spoofed line
|
|
45
|
+
* first and that IP last still `429`s. Raw socket only (`fetch` merges
|
|
46
|
+
* repeated headers into one line), so it runs when `base_url` is given.
|
|
27
47
|
* - **a success does not refund the per-IP budget** — the distributed-spray
|
|
28
48
|
* backstop. Failed guesses against *distinct* usernames from one forwarded
|
|
29
49
|
* IP, interleaved with the attacker's own successful logins, must still 429
|
|
@@ -50,7 +70,11 @@ import '../assert_dev_env.js';
|
|
|
50
70
|
*/
|
|
51
71
|
import { describe, test, assert } from 'vitest';
|
|
52
72
|
import { ERROR_RATE_LIMIT_EXCEEDED } from '../../http/error_schemas.js';
|
|
73
|
+
import { raw_http_exchange, raw_http_parse_response } from '../transports/raw_http.js';
|
|
53
74
|
import { default_login_account_rate_limit, default_login_ip_rate_limit } from '../../rate_limiter.js';
|
|
75
|
+
import { AuditLogListOutput } from '../../auth/admin_action_specs.js';
|
|
76
|
+
import { cross_rpc_call, expect_output } from './cell_cross_helpers.js';
|
|
77
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
54
78
|
/**
|
|
55
79
|
* RFC 5737 TEST-NET-3 (`203.0.113.0/24`) documentation addresses — guaranteed
|
|
56
80
|
* non-routable, so they never collide with a real client IP. One per bucket the
|
|
@@ -63,11 +87,35 @@ const XFF_IP_LIMIT = '203.0.113.1';
|
|
|
63
87
|
const XFF_SEGREGATION_EXHAUST = '203.0.113.2';
|
|
64
88
|
const XFF_SEGREGATION_FRESH = '203.0.113.3';
|
|
65
89
|
const XFF_SPRAY = '203.0.113.4';
|
|
90
|
+
const XFF_BURST = '203.0.113.5';
|
|
91
|
+
const XFF_CORRECT_BURST = '203.0.113.7';
|
|
92
|
+
const XFF_REPEATED_LINES = '203.0.113.6';
|
|
93
|
+
/** RFC 5737 TEST-NET-2 — the address a client writes into its own first line. */
|
|
94
|
+
const XFF_SPOOFED_LINE = '198.51.100.77';
|
|
95
|
+
/**
|
|
96
|
+
* RFC 3849 documentation (`2001:db8::/32`) addresses: two in one `/64` — the
|
|
97
|
+
* second differs past bit 64 only — and one in the next `/64`.
|
|
98
|
+
*/
|
|
99
|
+
const XFF_V6_EXHAUST = '2001:db8:e3:1::1';
|
|
100
|
+
const XFF_V6_SAME_64 = '2001:db8:e3:1:8000::2';
|
|
101
|
+
const XFF_V6_OTHER_64 = '2001:db8:e3:2::1';
|
|
102
|
+
/**
|
|
103
|
+
* Two addresses in one more `/64`: login exhausts the first, and a password
|
|
104
|
+
* change — which reserves on the same per-IP login limiter — comes from the
|
|
105
|
+
* second.
|
|
106
|
+
*/
|
|
107
|
+
const XFF_V6_PASSWORD_EXHAUST = '2001:db8:e3:5::1';
|
|
108
|
+
const XFF_V6_PASSWORD_SAME_64 = '2001:db8:e3:5:8000::2';
|
|
109
|
+
/** How long a raw exchange waits for the login's answer. */
|
|
110
|
+
const RAW_READ_TIMEOUT_MS = 5000;
|
|
66
111
|
/** A non-routable, non-existent identifier — login resolves to the not-found path (records the bucket, returns 401). */
|
|
67
112
|
const PROBE_PASSWORD = 'login_security_wrong_password';
|
|
68
113
|
export const describe_login_security_cross_tests = (options) => {
|
|
69
114
|
const { setup_test } = options;
|
|
70
115
|
const login_path = options.login_path ?? '/api/account/login';
|
|
116
|
+
const password_path = options.password_path ?? '/api/account/password';
|
|
117
|
+
const concurrent_transactions = options.concurrent_transactions ?? false;
|
|
118
|
+
const base_url = options.base_url ?? null;
|
|
71
119
|
/** Per-IP cap shared by both impls (TS `default_login_ip_rate_limit`, Rust `DEFAULT_LOGIN_IP_RATE_LIMIT`). */
|
|
72
120
|
const ip_limit = default_login_ip_rate_limit.max_attempts;
|
|
73
121
|
// The XFF-segregation case enables both limiters (one flag drives both), so
|
|
@@ -145,6 +193,112 @@ export const describe_login_security_cross_tests = (options) => {
|
|
|
145
193
|
const fresh = await attempt(fixture, XFF_SEGREGATION_FRESH, 'probe_xff');
|
|
146
194
|
assert.strictEqual(fresh.status, 401, "a fresh forwarded IP must not inherit another IP's limit");
|
|
147
195
|
});
|
|
196
|
+
test('IPv6 clients share a bucket per /64, and the audit row keeps the full address', async () => {
|
|
197
|
+
const fixture = await setup_test();
|
|
198
|
+
for (let i = 0; i < ip_limit; i++) {
|
|
199
|
+
const outcome = await attempt(fixture, XFF_V6_EXHAUST, 'probe_v6');
|
|
200
|
+
assert.strictEqual(outcome.status, 401, `attempt ${i + 1} from ${XFF_V6_EXHAUST}`);
|
|
201
|
+
}
|
|
202
|
+
// another address in the same /64 draws the same budget
|
|
203
|
+
const same = await attempt(fixture, XFF_V6_SAME_64, 'probe_v6');
|
|
204
|
+
assert.strictEqual(same.status, 429, 'an address in the same /64 must be rate-limited');
|
|
205
|
+
// the next /64 has its own
|
|
206
|
+
const other = await attempt(fixture, XFF_V6_OTHER_64, 'probe_v6');
|
|
207
|
+
assert.strictEqual(other.status, 401, 'the next /64 must keep its own budget');
|
|
208
|
+
// The limiter keys the /64; the audit trail keeps the address. Drain
|
|
209
|
+
// the fire-and-forget audit writes, then read this test's login rows
|
|
210
|
+
// (`_testing_reset` wiped the log at setup).
|
|
211
|
+
const drained = await cross_rpc_call(fixture.fresh_transport({ origin: null }), SPINE_RPC_PATH, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
|
|
212
|
+
assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
|
|
213
|
+
const listed = expect_output(await cross_rpc_call(fixture.transport, SPINE_RPC_PATH, 'audit_log_list', { event_type: 'login', outcome: 'failure' }, fixture.create_session_headers()), AuditLogListOutput);
|
|
214
|
+
const ips = listed.events.map((e) => e.ip);
|
|
215
|
+
assert.ok(ips.includes(XFF_V6_EXHAUST), `audit ips: ${JSON.stringify(ips)}`);
|
|
216
|
+
assert.ok(ips.includes(XFF_V6_OTHER_64), `audit ips: ${JSON.stringify(ips)}`);
|
|
217
|
+
assert.ok(ips.every((ip) => ip === null || !ip.includes('/')), `no audit row may carry a rate-limit key: ${JSON.stringify(ips)}`);
|
|
218
|
+
});
|
|
219
|
+
test('a password change from the /64 an exhausted IPv6 login shares is rate-limited', async () => {
|
|
220
|
+
const fixture = await setup_test();
|
|
221
|
+
for (let i = 0; i < ip_limit; i++) {
|
|
222
|
+
const outcome = await attempt(fixture, XFF_V6_PASSWORD_EXHAUST, 'probe_v6_password');
|
|
223
|
+
assert.strictEqual(outcome.status, 401, `attempt ${i + 1} from ${XFF_V6_PASSWORD_EXHAUST}`);
|
|
224
|
+
}
|
|
225
|
+
// The keeper's session, from another address in the same /64: the
|
|
226
|
+
// password route reserves on the per-IP login limiter before it
|
|
227
|
+
// reads the credential or the body, so the /64's spent budget
|
|
228
|
+
// answers. Keyed on the full address instead, the wrong
|
|
229
|
+
// `current_password` would answer `401`.
|
|
230
|
+
const res = await fixture.fresh_transport()(password_path, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: fixture.create_session_headers({
|
|
233
|
+
'content-type': 'application/json',
|
|
234
|
+
'x-forwarded-for': XFF_V6_PASSWORD_SAME_64
|
|
235
|
+
}),
|
|
236
|
+
body: JSON.stringify({
|
|
237
|
+
current_password: PROBE_PASSWORD,
|
|
238
|
+
new_password: 'login_security_new_password'
|
|
239
|
+
})
|
|
240
|
+
});
|
|
241
|
+
const body = (await res.json().catch(() => undefined));
|
|
242
|
+
assert.strictEqual(res.status, 429, `password change answered ${JSON.stringify(body)}`);
|
|
243
|
+
assert.strictEqual(body?.error, ERROR_RATE_LIMIT_EXCEEDED);
|
|
244
|
+
});
|
|
245
|
+
test(`a concurrent burst of ${ip_limit * 2} wrong logins gets at most ${ip_limit} past the per-IP limit`, async () => {
|
|
246
|
+
const fixture = await setup_test();
|
|
247
|
+
// All sent at once, so every request reaches the limiter before any
|
|
248
|
+
// has been answered. Its own username keeps the per-account bucket
|
|
249
|
+
// (cap above the IP cap, asserted above) out of the count.
|
|
250
|
+
const outcomes = await Promise.all(Array.from({ length: ip_limit * 2 }, () => attempt(fixture, XFF_BURST, 'probe_burst')));
|
|
251
|
+
const statuses = outcomes.map((o) => o.status);
|
|
252
|
+
const refused = statuses.filter((status) => status === 401).length;
|
|
253
|
+
const limited = statuses.filter((status) => status === 429).length;
|
|
254
|
+
// Exactly the cap: every admitted attempt fails and records, and the
|
|
255
|
+
// ones past it — waiting for a slot — are refused as those failures
|
|
256
|
+
// record, so the count is the same however the burst interleaves.
|
|
257
|
+
assert.strictEqual(refused, ip_limit, `exactly ${ip_limit} may reach the verify (${statuses})`);
|
|
258
|
+
assert.strictEqual(refused + limited, ip_limit * 2, `the rest must be 429 (${statuses})`);
|
|
259
|
+
// And the window is spent: the burst can't have left budget behind.
|
|
260
|
+
const after = await attempt(fixture, XFF_BURST, 'probe_burst');
|
|
261
|
+
assert.strictEqual(after.status, 429, 'the burst must leave the per-IP bucket full');
|
|
262
|
+
});
|
|
263
|
+
test.skipIf(!concurrent_transactions)(`a concurrent burst of ${ip_limit * 2} correct logins all succeed past the per-IP cap`, async () => {
|
|
264
|
+
const fixture = await setup_test();
|
|
265
|
+
const account = await fixture.create_account({
|
|
266
|
+
username: 'burst_correct',
|
|
267
|
+
password_value: 'burst_correct_password'
|
|
268
|
+
});
|
|
269
|
+
const statuses = await Promise.all(Array.from({ length: ip_limit * 2 }, () => succeed(fixture, XFF_CORRECT_BURST, account.account.username, 'burst_correct_password')));
|
|
270
|
+
assert.deepStrictEqual(statuses, Array(ip_limit * 2).fill(200), 'the logins past the cap wait for a slot rather than being refused');
|
|
271
|
+
});
|
|
272
|
+
test.skipIf(base_url === null)('every X-Forwarded-For line is read (a spoofed first line picks no bucket)', async () => {
|
|
273
|
+
assert.ok(base_url !== null);
|
|
274
|
+
const fixture = await setup_test();
|
|
275
|
+
for (let i = 0; i < ip_limit; i++) {
|
|
276
|
+
await attempt(fixture, XFF_REPEATED_LINES, 'probe_xff_lines');
|
|
277
|
+
}
|
|
278
|
+
const exhausted = await attempt(fixture, XFF_REPEATED_LINES, 'probe_xff_lines');
|
|
279
|
+
assert.strictEqual(exhausted.status, 429, 'the forwarded IP must be exhausted first');
|
|
280
|
+
// The client's own line first, the address the proxy appended last.
|
|
281
|
+
// A backend that read only the first line would key this on the
|
|
282
|
+
// fresh spoofed address and answer 401.
|
|
283
|
+
const url = new URL(base_url);
|
|
284
|
+
const body = JSON.stringify({ username: 'probe_xff_lines', password: PROBE_PASSWORD });
|
|
285
|
+
const request = `POST ${login_path} HTTP/1.1\r\n` +
|
|
286
|
+
`host: ${url.host}\r\n` +
|
|
287
|
+
`content-type: application/json\r\n` +
|
|
288
|
+
`x-forwarded-for: ${XFF_SPOOFED_LINE}\r\n` +
|
|
289
|
+
`x-forwarded-for: ${XFF_REPEATED_LINES}\r\n` +
|
|
290
|
+
`content-length: ${body.length}\r\n` +
|
|
291
|
+
`connection: close\r\n` +
|
|
292
|
+
`\r\n` +
|
|
293
|
+
body;
|
|
294
|
+
const { received } = await raw_http_exchange(base_url, request, {
|
|
295
|
+
read_timeout_ms: RAW_READ_TIMEOUT_MS,
|
|
296
|
+
until: 'response'
|
|
297
|
+
});
|
|
298
|
+
const response = raw_http_parse_response(received);
|
|
299
|
+
assert.ok(response, `no whole response; received: ${received.slice(0, 200)}`);
|
|
300
|
+
assert.strictEqual(response.status, 429, `the request must key on the proxy-appended line (body: ${response.body})`);
|
|
301
|
+
});
|
|
148
302
|
test('a success does not refund the per-IP budget (distributed-spray backstop)', async () => {
|
|
149
303
|
const fixture = await setup_test();
|
|
150
304
|
// The attacker's own account. Minting rides the setup path from
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import { type RpcCallApp } from '../rpc_helpers.js';
|
|
3
|
+
/** Options for `mint_narrowed_api_token`. */
|
|
4
|
+
export interface MintNarrowedApiTokenOptions {
|
|
5
|
+
/** Transport the mint goes over — it must carry an allowed `Origin` (the default `rpc_call` one does). */
|
|
6
|
+
readonly app: RpcCallApp;
|
|
7
|
+
/** RPC endpoint path `account_token_create` is mounted on. */
|
|
8
|
+
readonly rpc_path: string;
|
|
9
|
+
/** Session credential headers of the account that owns the token — the mint is session-only. */
|
|
10
|
+
readonly session_headers: Record<string, string>;
|
|
11
|
+
/** The RPC methods the token admits; every other method and every non-RPC surface refuses it. */
|
|
12
|
+
readonly methods: ReadonlyArray<string>;
|
|
13
|
+
/** Token label. Default `'narrowed test token'`. */
|
|
14
|
+
readonly name?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Mint a narrowed api token and return its raw bearer value.
|
|
18
|
+
*
|
|
19
|
+
* @returns the raw token, for an `Authorization: Bearer` header
|
|
20
|
+
* @throws Error when the mint is refused, naming the status and error
|
|
21
|
+
*/
|
|
22
|
+
export declare const mint_narrowed_api_token: (options: MintNarrowedApiTokenOptions) => Promise<string>;
|
|
23
|
+
//# sourceMappingURL=narrowed_api_token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"narrowed_api_token.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/narrowed_api_token.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiB9B,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEvE,6CAA6C;AAC7C,MAAM,WAAW,2BAA2B;IAC3C,0GAA0G;IAC1G,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gGAAgG;IAChG,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjD,iGAAiG;IACjG,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC,oDAAoD;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,SAAS,2BAA2B,KAClC,OAAO,CAAC,MAAM,CAmBhB,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mint a narrowed (method-scoped) api token through the production
|
|
4
|
+
* `account_token_create` path, for suites that probe what a narrowed token is
|
|
5
|
+
* refused.
|
|
6
|
+
*
|
|
7
|
+
* Minted rather than seeded: the token comes out of the same handler a real
|
|
8
|
+
* minter uses, on whichever account owns the session the caller passes — so a
|
|
9
|
+
* suite can put a narrowed token on an account that lacks a gating role, and
|
|
10
|
+
* the mint itself asserts that a narrowed scope is accepted at the mint
|
|
11
|
+
* boundary, not merely stored.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { account_token_create_action_spec } from '../../auth/account_action_specs.js';
|
|
16
|
+
import { rpc_call_for_spec } from '../rpc_helpers.js';
|
|
17
|
+
/**
|
|
18
|
+
* Mint a narrowed api token and return its raw bearer value.
|
|
19
|
+
*
|
|
20
|
+
* @returns the raw token, for an `Authorization: Bearer` header
|
|
21
|
+
* @throws Error when the mint is refused, naming the status and error
|
|
22
|
+
*/
|
|
23
|
+
export const mint_narrowed_api_token = async (options) => {
|
|
24
|
+
const { app, rpc_path, session_headers, methods, name = 'narrowed test token' } = options;
|
|
25
|
+
const res = await rpc_call_for_spec({
|
|
26
|
+
app,
|
|
27
|
+
path: rpc_path,
|
|
28
|
+
spec: account_token_create_action_spec,
|
|
29
|
+
params: {
|
|
30
|
+
name,
|
|
31
|
+
scope: { kind: 'methods', methods: [...methods] },
|
|
32
|
+
lifetime: { kind: 'eternal' }
|
|
33
|
+
},
|
|
34
|
+
headers: session_headers
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
throw new Error(`minting a narrowed api token failed with ${res.status}: ${JSON.stringify(res.error)}`);
|
|
38
|
+
}
|
|
39
|
+
return res.result.token;
|
|
40
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"origin.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/origin.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"origin.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/origin.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6B9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAM9D,eAAO,MAAM,2BAA2B,GAAI,SAAS,sBAAsB,KAAG,IAqB7E,CAAC"}
|
|
@@ -3,18 +3,17 @@ import '../assert_dev_env.js';
|
|
|
3
3
|
* Cross-backend parity suite for Origin verification.
|
|
4
4
|
*
|
|
5
5
|
* Origin checking is middleware that runs *before* the RPC dispatcher and
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* imperative suite
|
|
9
|
-
* in-process origin tests were already hand-rolled. Two cases:
|
|
6
|
+
* refuses only an `Origin` it doesn't allow. The case here needs a transport
|
|
7
|
+
* that sends no `Origin` at all (`fresh_transport({origin: null})`), so this
|
|
8
|
+
* is an imperative suite rather than a conformance-table row. The case:
|
|
10
9
|
*
|
|
11
|
-
* - **disallowed `Origin` → 403** `forbidden_origin`, refused before any
|
|
12
|
-
* handler runs (the allowlist rejects the cross-origin request even with
|
|
13
|
-
* a valid session cookie attached).
|
|
14
10
|
* - **absent `Origin` → request passes** — non-browser / direct-access
|
|
15
11
|
* clients (curl, CLI, server-to-server) carry no `Origin` and must not be
|
|
16
12
|
* blocked; token auth is the control for those callers.
|
|
17
13
|
*
|
|
14
|
+
* A disallowed `Origin` → 403 `forbidden_origin`, on every transport, is
|
|
15
|
+
* pinned by the front-door table (`transport_composition.ts`).
|
|
16
|
+
*
|
|
18
17
|
* Runs both legs via the shared `setup_test` protocol: the
|
|
19
18
|
* in-process leg (`auth/origin_parity.db.test.ts`, plain `gro test`) and the
|
|
20
19
|
* cross-process leg (`cross_backend/origin.cross.test.ts`, the TS spine
|
|
@@ -25,7 +24,6 @@ import '../assert_dev_env.js';
|
|
|
25
24
|
*/
|
|
26
25
|
import { describe, test, assert } from 'vitest';
|
|
27
26
|
import { account_verify_action_spec } from '../../auth/account_action_specs.js';
|
|
28
|
-
import { ERROR_FORBIDDEN_ORIGIN } from '../../http/error_schemas.js';
|
|
29
27
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
30
28
|
/** Build the JSON-RPC envelope body for a nullary `account_verify` call. */
|
|
31
29
|
const verify_envelope = (id) => JSON.stringify({ jsonrpc: '2.0', method: account_verify_action_spec.method, id });
|
|
@@ -33,24 +31,6 @@ export const describe_origin_cross_tests = (options) => {
|
|
|
33
31
|
const { setup_test } = options;
|
|
34
32
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
35
33
|
describe('origin verification parity', () => {
|
|
36
|
-
test('disallowed Origin → 403 forbidden_origin (refused before dispatch)', async () => {
|
|
37
|
-
const fixture = await setup_test();
|
|
38
|
-
// Keeper session cookie attached + a rogue Origin header (overrides
|
|
39
|
-
// the transport's default allowed Origin). The allowlist must reject
|
|
40
|
-
// before the dispatcher, returning a flat REST error body.
|
|
41
|
-
const res = await fixture.transport(rpc_path, {
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: {
|
|
44
|
-
...fixture.create_session_headers(),
|
|
45
|
-
origin: 'http://evil.com',
|
|
46
|
-
'content-type': 'application/json'
|
|
47
|
-
},
|
|
48
|
-
body: verify_envelope('evil-origin')
|
|
49
|
-
});
|
|
50
|
-
assert.strictEqual(res.status, 403, 'disallowed Origin must be rejected with 403');
|
|
51
|
-
const body = (await res.json().catch(() => undefined));
|
|
52
|
-
assert.strictEqual(body?.error, ERROR_FORBIDDEN_ORIGIN);
|
|
53
|
-
});
|
|
54
34
|
test('absent Origin → request passes (non-browser direct access)', async () => {
|
|
55
35
|
const fixture = await setup_test();
|
|
56
36
|
// `origin: null` so no Origin header is sent at all (a header omission
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { RpcPathCrossSuiteOptions } from './setup.js';
|
|
4
|
+
import { TestingOriginCheckOutput, type TestingOriginCheckInput } from './testing_reset_actions.js';
|
|
5
|
+
/** One expected refusal of a parse row. */
|
|
6
|
+
export declare const OriginConformanceError: z.ZodObject<{
|
|
7
|
+
index: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
kind: z.ZodEnum<{
|
|
9
|
+
empty_entry: "empty_entry";
|
|
10
|
+
not_printable_ascii: "not_printable_ascii";
|
|
11
|
+
missing_scheme: "missing_scheme";
|
|
12
|
+
unsupported_scheme: "unsupported_scheme";
|
|
13
|
+
has_path: "has_path";
|
|
14
|
+
has_userinfo: "has_userinfo";
|
|
15
|
+
missing_host: "missing_host";
|
|
16
|
+
invalid_host_char: "invalid_host_char";
|
|
17
|
+
unbracketed_ipv6: "unbracketed_ipv6";
|
|
18
|
+
partial_wildcard: "partial_wildcard";
|
|
19
|
+
non_canonical_ipv4: "non_canonical_ipv4";
|
|
20
|
+
unclosed_ipv6: "unclosed_ipv6";
|
|
21
|
+
invalid_ipv6: "invalid_ipv6";
|
|
22
|
+
invalid_after_ipv6: "invalid_after_ipv6";
|
|
23
|
+
invalid_port: "invalid_port";
|
|
24
|
+
default_port: "default_port";
|
|
25
|
+
no_origins: "no_origins";
|
|
26
|
+
}>;
|
|
27
|
+
}, z.core.$strict>;
|
|
28
|
+
export type OriginConformanceError = z.infer<typeof OriginConformanceError>;
|
|
29
|
+
/** A parse row: the value parses (`ok: true`), or is refused with `errors`. */
|
|
30
|
+
export declare const OriginConformanceParseRow: z.ZodUnion<readonly [z.ZodObject<{
|
|
31
|
+
raw: z.ZodString;
|
|
32
|
+
ok: z.ZodLiteral<true>;
|
|
33
|
+
note: z.ZodOptional<z.ZodString>;
|
|
34
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
35
|
+
raw: z.ZodString;
|
|
36
|
+
errors: z.ZodArray<z.ZodObject<{
|
|
37
|
+
index: z.ZodOptional<z.ZodNumber>;
|
|
38
|
+
kind: z.ZodEnum<{
|
|
39
|
+
empty_entry: "empty_entry";
|
|
40
|
+
not_printable_ascii: "not_printable_ascii";
|
|
41
|
+
missing_scheme: "missing_scheme";
|
|
42
|
+
unsupported_scheme: "unsupported_scheme";
|
|
43
|
+
has_path: "has_path";
|
|
44
|
+
has_userinfo: "has_userinfo";
|
|
45
|
+
missing_host: "missing_host";
|
|
46
|
+
invalid_host_char: "invalid_host_char";
|
|
47
|
+
unbracketed_ipv6: "unbracketed_ipv6";
|
|
48
|
+
partial_wildcard: "partial_wildcard";
|
|
49
|
+
non_canonical_ipv4: "non_canonical_ipv4";
|
|
50
|
+
unclosed_ipv6: "unclosed_ipv6";
|
|
51
|
+
invalid_ipv6: "invalid_ipv6";
|
|
52
|
+
invalid_after_ipv6: "invalid_after_ipv6";
|
|
53
|
+
invalid_port: "invalid_port";
|
|
54
|
+
default_port: "default_port";
|
|
55
|
+
no_origins: "no_origins";
|
|
56
|
+
}>;
|
|
57
|
+
}, z.core.$strict>>;
|
|
58
|
+
note: z.ZodOptional<z.ZodString>;
|
|
59
|
+
}, z.core.$strict>]>;
|
|
60
|
+
export type OriginConformanceParseRow = z.infer<typeof OriginConformanceParseRow>;
|
|
61
|
+
/** A match row: a valid allowlist, one `Origin` value or `null`, and the answer. */
|
|
62
|
+
export declare const OriginConformanceMatchRow: z.ZodObject<{
|
|
63
|
+
patterns: z.ZodString;
|
|
64
|
+
origin: z.ZodNullable<z.ZodString>;
|
|
65
|
+
allowed: z.ZodBoolean;
|
|
66
|
+
note: z.ZodOptional<z.ZodString>;
|
|
67
|
+
}, z.core.$strict>;
|
|
68
|
+
export type OriginConformanceMatchRow = z.infer<typeof OriginConformanceMatchRow>;
|
|
69
|
+
/** The conformance table. */
|
|
70
|
+
export declare const OriginConformance: z.ZodObject<{
|
|
71
|
+
parse: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
72
|
+
raw: z.ZodString;
|
|
73
|
+
ok: z.ZodLiteral<true>;
|
|
74
|
+
note: z.ZodOptional<z.ZodString>;
|
|
75
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
76
|
+
raw: z.ZodString;
|
|
77
|
+
errors: z.ZodArray<z.ZodObject<{
|
|
78
|
+
index: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
kind: z.ZodEnum<{
|
|
80
|
+
empty_entry: "empty_entry";
|
|
81
|
+
not_printable_ascii: "not_printable_ascii";
|
|
82
|
+
missing_scheme: "missing_scheme";
|
|
83
|
+
unsupported_scheme: "unsupported_scheme";
|
|
84
|
+
has_path: "has_path";
|
|
85
|
+
has_userinfo: "has_userinfo";
|
|
86
|
+
missing_host: "missing_host";
|
|
87
|
+
invalid_host_char: "invalid_host_char";
|
|
88
|
+
unbracketed_ipv6: "unbracketed_ipv6";
|
|
89
|
+
partial_wildcard: "partial_wildcard";
|
|
90
|
+
non_canonical_ipv4: "non_canonical_ipv4";
|
|
91
|
+
unclosed_ipv6: "unclosed_ipv6";
|
|
92
|
+
invalid_ipv6: "invalid_ipv6";
|
|
93
|
+
invalid_after_ipv6: "invalid_after_ipv6";
|
|
94
|
+
invalid_port: "invalid_port";
|
|
95
|
+
default_port: "default_port";
|
|
96
|
+
no_origins: "no_origins";
|
|
97
|
+
}>;
|
|
98
|
+
}, z.core.$strict>>;
|
|
99
|
+
note: z.ZodOptional<z.ZodString>;
|
|
100
|
+
}, z.core.$strict>]>>;
|
|
101
|
+
match: z.ZodArray<z.ZodObject<{
|
|
102
|
+
patterns: z.ZodString;
|
|
103
|
+
origin: z.ZodNullable<z.ZodString>;
|
|
104
|
+
allowed: z.ZodBoolean;
|
|
105
|
+
note: z.ZodOptional<z.ZodString>;
|
|
106
|
+
}, z.core.$strict>>;
|
|
107
|
+
}, z.core.$strict>;
|
|
108
|
+
export type OriginConformance = z.infer<typeof OriginConformance>;
|
|
109
|
+
/** The committed table, resolved relative to this module. */
|
|
110
|
+
export declare const ORIGIN_CONFORMANCE_URL: URL;
|
|
111
|
+
/**
|
|
112
|
+
* Read and validate the committed conformance table.
|
|
113
|
+
*
|
|
114
|
+
* @returns the parsed table
|
|
115
|
+
* @throws Error when the file doesn't match `OriginConformance`
|
|
116
|
+
*/
|
|
117
|
+
export declare const load_origin_conformance: () => OriginConformance;
|
|
118
|
+
/** One table row as the action input that checks it. */
|
|
119
|
+
export interface OriginConformanceCheck {
|
|
120
|
+
/** A short label naming the row, for a failure message. */
|
|
121
|
+
readonly label: string;
|
|
122
|
+
readonly input: TestingOriginCheckInput;
|
|
123
|
+
readonly expected: TestingOriginCheckOutput;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Flatten the table into action inputs and the answers they must get. A parse
|
|
127
|
+
* row sends a `null` origin, so a value that parses answers
|
|
128
|
+
* `{ok: true, allowed: true}`.
|
|
129
|
+
*
|
|
130
|
+
* @param table - the conformance table
|
|
131
|
+
* @returns one check per row, parse rows first
|
|
132
|
+
*/
|
|
133
|
+
export declare const to_origin_conformance_checks: (table: OriginConformance) => Array<OriginConformanceCheck>;
|
|
134
|
+
/**
|
|
135
|
+
* Compare each check's answer with its expectation and fail once, listing
|
|
136
|
+
* every row that disagrees.
|
|
137
|
+
*
|
|
138
|
+
* @param checks - the table's checks
|
|
139
|
+
* @param answer - the answer each check got, in order
|
|
140
|
+
* @param backend - the name the failure message gives the answerer
|
|
141
|
+
*/
|
|
142
|
+
export declare const assert_origin_conformance: (checks: ReadonlyArray<OriginConformanceCheck>, answer: ReadonlyArray<unknown>, backend: string) => void;
|
|
143
|
+
/** Options for `describe_origin_conformance_cross_tests`. */
|
|
144
|
+
export type OriginConformanceCrossTestOptions = RpcPathCrossSuiteOptions;
|
|
145
|
+
/**
|
|
146
|
+
* Register the origin conformance suite: every row of the committed table,
|
|
147
|
+
* sent to the backend's `_testing_origin_check` action over the keeper's
|
|
148
|
+
* daemon-token channel, must get the answer the table names. The action is on
|
|
149
|
+
* every spine test binary, so the suite is ungated.
|
|
150
|
+
*/
|
|
151
|
+
export declare const describe_origin_conformance_cross_tests: (options: OriginConformanceCrossTestOptions) => void;
|
|
152
|
+
//# sourceMappingURL=origin_conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origin_conformance.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/origin_conformance.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0B9B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE3D,OAAO,EACN,wBAAwB,EAExB,KAAK,uBAAuB,EAC5B,MAAM,4BAA4B,CAAC;AAEpC,2CAA2C;AAC3C,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;kBAGjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAOpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,oFAAoF;AACpF,eAAO,MAAM,yBAAyB;;;;;kBAKpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,6BAA6B;AAC7B,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAG5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,6DAA6D;AAC7D,eAAO,MAAM,sBAAsB,EAAE,GAA2D,CAAC;AAEjG;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,iBACuC,CAAC;AAEnF,wDAAwD;AACxD,MAAM,WAAW,sBAAsB;IACtC,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,uBAAuB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;CAC5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,GACxC,OAAO,iBAAiB,KACtB,KAAK,CAAC,sBAAsB,CAc9B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,GACrC,QAAQ,aAAa,CAAC,sBAAsB,CAAC,EAC7C,QAAQ,aAAa,CAAC,OAAO,CAAC,EAC9B,SAAS,MAAM,KACb,IAiBF,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,iCAAiC,GAAG,wBAAwB,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,uCAAuC,GACnD,SAAS,iCAAiC,KACxC,IA4BF,CAAC"}
|