@fuzdev/fuz_app 0.147.0 → 0.148.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +6 -4
- package/dist/actions/action_codegen.d.ts +15 -1
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +23 -2
- package/dist/actions/perform_action.js +1 -1
- package/dist/auth/cell_action_specs.d.ts +3 -1
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +5 -5
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +2 -3
- package/dist/auth/cell_data_schema.d.ts +1 -1
- package/dist/auth/cell_data_schema.js +1 -1
- package/dist/auth/cell_field_action_specs.d.ts +4 -3
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +5 -6
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +8 -1
- package/dist/auth/cell_item_action_specs.d.ts +4 -2
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +5 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +8 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts +2 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +3 -1
- package/dist/primitive_schemas.d.ts +1 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +2 -1
- package/dist/schema_meta.d.ts +20 -0
- package/dist/schema_meta.d.ts.map +1 -1
- package/dist/schema_meta.js +7 -1
- package/dist/testing/CLAUDE.md +720 -114
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +73 -5
- package/dist/testing/adversarial_input.d.ts +0 -47
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +4 -340
- package/dist/testing/adversarial_input_cases.d.ts +64 -0
- package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
- package/dist/testing/adversarial_input_cases.js +348 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +24 -20
- package/dist/testing/audit_drift_guard.d.ts +27 -1
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +50 -0
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +29 -12
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/actor_lookup.js +7 -25
- package/dist/testing/cross_backend/app_settings.d.ts.map +1 -1
- package/dist/testing/cross_backend/app_settings.js +4 -6
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts +51 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/audit_cross_helpers.js +78 -0
- package/dist/testing/cross_backend/backend_config.d.ts +2 -1
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +6 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +26 -44
- package/dist/testing/cross_backend/body_size_smuggling.d.ts +5 -0
- package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.js +7 -14
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +3 -1
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +55 -59
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +20 -23
- package/dist/testing/cross_backend/cargo_binary.d.ts +70 -0
- package/dist/testing/cross_backend/cargo_binary.d.ts.map +1 -0
- package/dist/testing/cross_backend/cargo_binary.js +148 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +57 -1
- package/dist/testing/cross_backend/cell_gated_create.d.ts +0 -60
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +9 -487
- package/dist/testing/cross_backend/cell_grant_role.d.ts +14 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +22 -12
- package/dist/testing/cross_backend/cell_moderate.d.ts +66 -0
- package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_moderate.js +511 -0
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_ref_admission.js +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +40 -19
- package/dist/testing/cross_backend/conformance_case.d.ts +43 -4
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +29 -1
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts +93 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.js +125 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +144 -4
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +297 -65
- package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -1
- package/dist/testing/cross_backend/cookie_attributes.js +3 -2
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +38 -5
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/create_cross_backend_global_setup.js +24 -3
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +5 -5
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.d.ts +10 -6
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.js +32 -31
- package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_header_robustness.js +4 -6
- package/dist/testing/cross_backend/{cell_cross_helpers.d.ts → cross_rpc.d.ts} +12 -2
- package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -0
- package/dist/testing/cross_backend/{cell_cross_helpers.js → cross_rpc.js} +31 -7
- package/dist/testing/cross_backend/default_backend_configs.d.ts +56 -61
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +97 -91
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +2 -2
- package/dist/testing/cross_backend/differential.d.ts +126 -0
- package/dist/testing/cross_backend/differential.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential.js +201 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts +150 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential_normalize.js +153 -0
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +27 -17
- package/dist/testing/cross_backend/full_spine_mount.d.ts +15 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +55 -1
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/identity_parity.js +6 -5
- package/dist/testing/cross_backend/in_process_setup.d.ts +12 -2
- package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/in_process_setup.js +10 -7
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +13 -18
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/origin_conformance.js +1 -1
- package/dist/testing/cross_backend/origin_scope.d.ts +6 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin_scope.js +13 -12
- package/dist/testing/cross_backend/peer_ping_ws.d.ts +2 -5
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +14 -24
- package/dist/testing/cross_backend/postgres_database.d.ts +85 -0
- package/dist/testing/cross_backend/postgres_database.d.ts.map +1 -0
- package/dist/testing/cross_backend/postgres_database.js +140 -0
- package/dist/testing/cross_backend/provided_context.d.ts +28 -0
- package/dist/testing/cross_backend/provided_context.d.ts.map +1 -0
- package/dist/testing/cross_backend/provided_context.js +35 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +10 -12
- package/dist/testing/cross_backend/ready.js +2 -2
- package/dist/testing/cross_backend/revocation_drivers.d.ts +4 -3
- package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -1
- package/dist/testing/cross_backend/revocation_drivers.js +1 -1
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +3 -2
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +203 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +2 -5
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +203 -111
- package/dist/testing/cross_backend/role_grant_participation.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +32 -21
- package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_role_grammar.js +6 -9
- package/dist/testing/cross_backend/rpc_adversarial.d.ts +38 -0
- package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_adversarial.js +292 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts +24 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.js +124 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +101 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.js +113 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +10 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +27 -18
- package/dist/testing/cross_backend/session_cap.d.ts +9 -8
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +9 -4
- package/dist/testing/cross_backend/session_cookie_codec.js +1 -1
- package/dist/testing/cross_backend/setup.d.ts +56 -54
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +29 -29
- package/dist/testing/cross_backend/simple_request.d.ts.map +1 -1
- package/dist/testing/cross_backend/simple_request.js +4 -4
- package/dist/testing/cross_backend/spawn_backend.d.ts +59 -10
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
- package/dist/testing/cross_backend/spawn_backend.js +219 -60
- package/dist/testing/cross_backend/spine_suites.d.ts +231 -0
- package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_suites.js +626 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +29 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +29 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +3 -4
- package/dist/testing/cross_backend/standard.d.ts +18 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +2 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +24 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +15 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts +2 -8
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +7 -10
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +3 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +8 -4
- package/dist/testing/cross_backend/testing_server_bun.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +1 -1
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +9 -13
- package/dist/testing/cross_backend/transport_composition.d.ts +26 -2
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -1
- package/dist/testing/cross_backend/transport_composition.js +81 -51
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +8 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +17 -10
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +2 -5
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +8 -14
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +4 -9
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_connection_cap.js +11 -12
- package/dist/testing/cross_backend/ws_endpoint.d.ts +52 -0
- package/dist/testing/cross_backend/ws_endpoint.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_endpoint.js +54 -0
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +2 -5
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_inbound_dispatch.js +9 -15
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +2 -7
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_outbound_overflow.js +5 -9
- package/dist/testing/cross_backend/ws_role_gated.d.ts +4 -5
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_role_gated.js +12 -21
- package/dist/testing/cross_backend/ws_round_trip.d.ts +12 -9
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +29 -34
- package/dist/testing/db_entities.d.ts +16 -3
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +28 -5
- package/dist/testing/integration_helpers.d.ts +61 -2
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +75 -18
- package/dist/testing/round_trip.d.ts +17 -0
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +67 -24
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +13 -138
- package/dist/testing/rpc_envelope_cases.d.ts +111 -0
- package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
- package/dist/testing/rpc_envelope_cases.js +240 -0
- package/dist/testing/rpc_helpers.d.ts +31 -0
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +35 -14
- package/dist/testing/rpc_round_trip.d.ts +62 -6
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +151 -39
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +22 -2
- package/dist/testing/standard.d.ts +26 -0
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +4 -1
- package/dist/testing/transports/raw_http.d.ts +21 -0
- package/dist/testing/transports/raw_http.d.ts.map +1 -1
- package/dist/testing/transports/raw_http.js +30 -0
- package/package.json +6 -4
- package/dist/testing/cross_backend/actor_search.d.ts +0 -6
- package/dist/testing/cross_backend/actor_search.d.ts.map +0 -1
- package/dist/testing/cross_backend/actor_search.js +0 -90
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +0 -1
|
@@ -26,7 +26,13 @@ import '../assert_dev_env.js';
|
|
|
26
26
|
* drives the lifecycle RPC over HTTP, then asserts the frame lands on that
|
|
27
27
|
* socket and strict-parses against the canonical wire schema — the guard
|
|
28
28
|
* against serialization drift (field / null / datetime / the flat revoke shape /
|
|
29
|
-
* the supersede `reason` + `cause_id`).
|
|
29
|
+
* the supersede `reason` + `cause_id`). Every case also holds a third account's
|
|
30
|
+
* admitted socket open — a bystander, party to nothing the case drives — and,
|
|
31
|
+
* once the targeted frame has landed, asserts the bystander received nothing
|
|
32
|
+
* but the answers to its own requests, behind a `heartbeat` barrier on its
|
|
33
|
+
* socket (`assert_bystander_heard_nothing` says why the barrier makes the
|
|
34
|
+
* silence meaningful). A fan-out that reached past its target account fails
|
|
35
|
+
* there.
|
|
30
36
|
*
|
|
31
37
|
* Sends are queued on the post-commit drain (handler-emit, not audit-derived),
|
|
32
38
|
* so a frame may land a beat after the RPC resolves — `WsClient.wait_for` polls
|
|
@@ -53,15 +59,17 @@ import '../assert_dev_env.js';
|
|
|
53
59
|
*/
|
|
54
60
|
import { assert, describe } from 'vitest';
|
|
55
61
|
import { rpc_call } from '../rpc_helpers.js';
|
|
56
|
-
import {
|
|
57
|
-
import { is_notification_with } from '../transports/ws_client.js';
|
|
62
|
+
import { WS_ADMISSION_PROBE_ID } from '../transports/ws_transport.js';
|
|
63
|
+
import { is_notification_with, is_response_for } from '../transports/ws_client.js';
|
|
58
64
|
import { ROLE_ADMIN } from '../../auth/role_schema.js';
|
|
59
65
|
import { WS_CLOSE_PERMISSION_REVOKED } from '../../actions/transports.js';
|
|
60
66
|
import { ROLE_GRANT_OFFER_RECEIVED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_ACCEPTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_DECLINED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_RETRACTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_SUPERSEDE_NOTIFICATION_METHOD, ROLE_GRANT_REVOKE_NOTIFICATION_METHOD, RoleGrantOfferReceivedParams, RoleGrantOfferAcceptedParams, RoleGrantOfferDeclinedParams, RoleGrantOfferRetractedParams, RoleGrantOfferSupersedeParams, RoleGrantRevokeParams } from '../../auth/role_grant_offer_notifications.js';
|
|
61
67
|
import { test_if } from './capabilities.js';
|
|
62
68
|
import { create_active_role_grant, create_pending_role_grant_offer } from './role_grant_cross_helpers.js';
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
70
|
+
import { open_session_ws } from './ws_endpoint.js';
|
|
71
|
+
/** The request id the bystander's barrier `heartbeat` goes out under. */
|
|
72
|
+
const BYSTANDER_BARRIER_ID = 'bystander_barrier';
|
|
65
73
|
/**
|
|
66
74
|
* Register the role-grant-offer WS notification suite — seven cases over a real
|
|
67
75
|
* upgrade, one per server-initiated notification (received / accepted /
|
|
@@ -71,28 +79,62 @@ const RPC_PATH = '/api/rpc';
|
|
|
71
79
|
* params schema. Gated on `capabilities.ws`.
|
|
72
80
|
*/
|
|
73
81
|
export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
74
|
-
const { setup_test, capabilities,
|
|
82
|
+
const { setup_test, capabilities, ws_required_roles = [] } = options;
|
|
75
83
|
const revoke_closes_the_socket = ws_required_roles.includes(ROLE_ADMIN);
|
|
76
84
|
// -- shared helpers -------------------------------------------------------
|
|
77
85
|
/**
|
|
78
|
-
* Open a WS transport
|
|
79
|
-
*
|
|
80
|
-
*
|
|
86
|
+
* Open a WS transport on an account's session, admitted before it resolves:
|
|
87
|
+
* a targeted notification is delivered only to an admitted connection, and
|
|
88
|
+
* the handshake alone doesn't say it is one.
|
|
81
89
|
*/
|
|
82
|
-
const open_ws = (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
const open_ws = (account) => open_session_ws(options, account, { admitted: true });
|
|
91
|
+
/**
|
|
92
|
+
* A third account's admitted socket — party to nothing a case drives. It
|
|
93
|
+
* holds `ROLE_ADMIN`, as every account here does, so it opens a socket
|
|
94
|
+
* gated on that role too. Opened before the lifecycle call, after the
|
|
95
|
+
* account's own offer and accept have run.
|
|
96
|
+
*/
|
|
97
|
+
const open_bystander = async (fixture) => {
|
|
98
|
+
const bystander = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
99
|
+
return open_ws(bystander);
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Assert the bystander's socket received nothing but the answers to its
|
|
103
|
+
* own requests. Called once the targeted party holds its frame, and
|
|
104
|
+
* meaningful only behind the barrier it sends first: a `heartbeat` round
|
|
105
|
+
* trip on the bystander's socket.
|
|
106
|
+
*
|
|
107
|
+
* Why the barrier makes the silence a finding: both spines queue one
|
|
108
|
+
* fan-out to every recipient in a single pass with no await in it (the TS
|
|
109
|
+
* transport's `broadcast_filtered` loop, the Rust registry's
|
|
110
|
+
* `deliver_all`), and neither writes a response ahead of a notification
|
|
111
|
+
* already queued for the same connection. On the TS spine responses and
|
|
112
|
+
* notifications share one outbound queue. On the Rust spine a response is
|
|
113
|
+
* written directly, but the loop's completion arm first writes every
|
|
114
|
+
* frame already queued for the connection (`flush_queued_frames` in
|
|
115
|
+
* `fuz_realtime`'s `ws_endpoint.rs`). So a frame the pass addressed to the
|
|
116
|
+
* bystander reaches it ahead of the barrier's response, and reading that
|
|
117
|
+
* response first means the pass sent it nothing. On the TS spine this is
|
|
118
|
+
* strict: a fan-out is sent in one synchronous closure. On the Rust spine
|
|
119
|
+
* `deliver_all` pushes to recipients one after another on one worker
|
|
120
|
+
* while the target's connection loop runs on another, so the target's
|
|
121
|
+
* frame can be written while the pass is still pushing — a window the
|
|
122
|
+
* barrier's round trip outlasts in practice. Either way the check covers
|
|
123
|
+
* every pass of the drain, not just the one that reached the target: TS
|
|
124
|
+
* runs them in one closure, and `PostCommitEffects::drain_all` joins
|
|
125
|
+
* effects whose notify futures push synchronously, so every pass
|
|
126
|
+
* completes in the drain's first poll.
|
|
127
|
+
*/
|
|
128
|
+
const assert_bystander_heard_nothing = async (bystander_ws) => {
|
|
129
|
+
await bystander_ws.request(BYSTANDER_BARRIER_ID, 'heartbeat', {}, 5000);
|
|
130
|
+
const strays = bystander_ws.messages.filter((msg) => !is_response_for(WS_ADMISSION_PROBE_ID)(msg) && !is_response_for(BYSTANDER_BARRIER_ID)(msg));
|
|
131
|
+
assert.deepStrictEqual(strays, [], 'a bystander socket received frames addressed to others');
|
|
90
132
|
};
|
|
91
133
|
/** Drive a JSON-RPC call over the fixture's HTTP transport. */
|
|
92
|
-
const rpc = (fixture, method, params, headers) => rpc_call({ app: fixture.transport, path:
|
|
134
|
+
const rpc = (fixture, method, params, headers) => rpc_call({ app: fixture.transport, path: SPINE_RPC_PATH, method, params, headers });
|
|
93
135
|
/** A fresh pending `ROLE_ADMIN` offer, from the keeper unless a grantor is given. */
|
|
94
136
|
const create_pending_offer = (fixture, to_account_id, grantor_headers) => create_pending_role_grant_offer(fixture, to_account_id, grantor_headers, {
|
|
95
|
-
rpc_path:
|
|
137
|
+
rpc_path: SPINE_RPC_PATH
|
|
96
138
|
});
|
|
97
139
|
// -- tests ----------------------------------------------------------------
|
|
98
140
|
describe('role_grant_offer WS notifications (cross-process)', () => {
|
|
@@ -104,77 +146,105 @@ export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
|
104
146
|
// earlier notification lands before the socket opens, so it's not
|
|
105
147
|
// the one we observe).
|
|
106
148
|
const recipient = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
107
|
-
const
|
|
149
|
+
const bystander_ws = await open_bystander(fixture);
|
|
108
150
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
151
|
+
const recipient_ws = await open_ws(recipient);
|
|
152
|
+
try {
|
|
153
|
+
// The keeper (grantor, holds ROLE_ADMIN) offers the recipient a
|
|
154
|
+
// role over RPC — fresh pending offer, emits the notification
|
|
155
|
+
// post-commit.
|
|
156
|
+
const created = await rpc(fixture, 'role_grant_offer_create', { to_account_id: recipient.account.id, role: ROLE_ADMIN }, fixture.create_session_headers());
|
|
157
|
+
assert.isTrue(created.ok, `offer create failed: ${JSON.stringify(created)}`);
|
|
158
|
+
const frame = await recipient_ws.wait_for(is_notification_with(ROLE_GRANT_OFFER_RECEIVED_NOTIFICATION_METHOD, (p) => p.offer.to_account_id === recipient.account.id), 5000);
|
|
159
|
+
// Params strict-parse against the canonical wire schema — guards
|
|
160
|
+
// the serialization against field/null/datetime drift.
|
|
161
|
+
const params = RoleGrantOfferReceivedParams.parse(frame.params);
|
|
162
|
+
assert.strictEqual(params.offer.to_account_id, recipient.account.id);
|
|
163
|
+
assert.strictEqual(params.offer.role, ROLE_ADMIN);
|
|
164
|
+
await assert_bystander_heard_nothing(bystander_ws);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await recipient_ws.close();
|
|
168
|
+
}
|
|
120
169
|
}
|
|
121
170
|
finally {
|
|
122
|
-
await
|
|
171
|
+
await bystander_ws.close();
|
|
123
172
|
}
|
|
124
173
|
});
|
|
125
174
|
test_if(capabilities.ws, 'accept delivers role_grant_offer_accepted to the grantor WS', async () => {
|
|
126
175
|
const fixture = await setup_test();
|
|
127
176
|
const recipient = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
128
177
|
// Grantor = keeper; open its socket BEFORE the recipient accepts.
|
|
129
|
-
const
|
|
178
|
+
const bystander_ws = await open_bystander(fixture);
|
|
130
179
|
try {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
180
|
+
const grantor = await open_ws(fixture);
|
|
181
|
+
try {
|
|
182
|
+
const offer = await create_pending_offer(fixture, recipient.account.id);
|
|
183
|
+
const accepted = await rpc(fixture, 'role_grant_offer_accept', { offer_id: offer.id }, recipient.create_session_headers());
|
|
184
|
+
assert.isTrue(accepted.ok, `accept failed: ${JSON.stringify(accepted)}`);
|
|
185
|
+
const frame = await grantor.wait_for(is_notification_with(ROLE_GRANT_OFFER_ACCEPTED_NOTIFICATION_METHOD, (p) => p.offer.id === offer.id), 5000);
|
|
186
|
+
const params = RoleGrantOfferAcceptedParams.parse(frame.params);
|
|
187
|
+
assert.strictEqual(params.offer.id, offer.id);
|
|
188
|
+
assert.isNotNull(params.offer.accepted_at);
|
|
189
|
+
await assert_bystander_heard_nothing(bystander_ws);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
await grantor.close();
|
|
193
|
+
}
|
|
138
194
|
}
|
|
139
195
|
finally {
|
|
140
|
-
await
|
|
196
|
+
await bystander_ws.close();
|
|
141
197
|
}
|
|
142
198
|
});
|
|
143
199
|
test_if(capabilities.ws, 'decline delivers role_grant_offer_declined to the grantor WS', async () => {
|
|
144
200
|
const fixture = await setup_test();
|
|
145
201
|
const recipient = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
146
|
-
const
|
|
202
|
+
const bystander_ws = await open_bystander(fixture);
|
|
147
203
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
204
|
+
const grantor = await open_ws(fixture);
|
|
205
|
+
try {
|
|
206
|
+
const offer = await create_pending_offer(fixture, recipient.account.id);
|
|
207
|
+
const declined = await rpc(fixture, 'role_grant_offer_decline', { offer_id: offer.id, reason: 'no thanks' }, recipient.create_session_headers());
|
|
208
|
+
assert.isTrue(declined.ok, `decline failed: ${JSON.stringify(declined)}`);
|
|
209
|
+
const frame = await grantor.wait_for(is_notification_with(ROLE_GRANT_OFFER_DECLINED_NOTIFICATION_METHOD, (p) => p.offer.id === offer.id), 5000);
|
|
210
|
+
const params = RoleGrantOfferDeclinedParams.parse(frame.params);
|
|
211
|
+
assert.strictEqual(params.offer.id, offer.id);
|
|
212
|
+
// Decline reason rides inside the offer row, not a sibling field.
|
|
213
|
+
assert.strictEqual(params.offer.decline_reason, 'no thanks');
|
|
214
|
+
await assert_bystander_heard_nothing(bystander_ws);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await grantor.close();
|
|
218
|
+
}
|
|
156
219
|
}
|
|
157
220
|
finally {
|
|
158
|
-
await
|
|
221
|
+
await bystander_ws.close();
|
|
159
222
|
}
|
|
160
223
|
});
|
|
161
224
|
test_if(capabilities.ws, 'retract delivers role_grant_offer_retracted to the recipient WS', async () => {
|
|
162
225
|
const fixture = await setup_test();
|
|
163
226
|
const recipient = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
164
227
|
// Recipient learns their pending offer was pulled — open its socket.
|
|
165
|
-
const
|
|
228
|
+
const bystander_ws = await open_bystander(fixture);
|
|
166
229
|
try {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
230
|
+
const recipient_ws = await open_ws(recipient);
|
|
231
|
+
try {
|
|
232
|
+
const offer = await create_pending_offer(fixture, recipient.account.id);
|
|
233
|
+
const retracted = await rpc(fixture, 'role_grant_offer_retract', { offer_id: offer.id }, fixture.create_session_headers());
|
|
234
|
+
assert.isTrue(retracted.ok, `retract failed: ${JSON.stringify(retracted)}`);
|
|
235
|
+
const frame = await recipient_ws.wait_for(is_notification_with(ROLE_GRANT_OFFER_RETRACTED_NOTIFICATION_METHOD, (p) => p.offer.id === offer.id), 5000);
|
|
236
|
+
const params = RoleGrantOfferRetractedParams.parse(frame.params);
|
|
237
|
+
assert.strictEqual(params.offer.id, offer.id);
|
|
238
|
+
assert.strictEqual(params.offer.to_account_id, recipient.account.id);
|
|
239
|
+
assert.isNotNull(params.offer.retracted_at);
|
|
240
|
+
await assert_bystander_heard_nothing(bystander_ws);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
await recipient_ws.close();
|
|
244
|
+
}
|
|
175
245
|
}
|
|
176
246
|
finally {
|
|
177
|
-
await
|
|
247
|
+
await bystander_ws.close();
|
|
178
248
|
}
|
|
179
249
|
});
|
|
180
250
|
test_if(capabilities.ws, revoke_closes_the_socket
|
|
@@ -183,38 +253,46 @@ export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
|
183
253
|
const fixture = await setup_test();
|
|
184
254
|
const revokee = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
185
255
|
const role_grant_id = await create_active_role_grant(fixture, revokee, {
|
|
186
|
-
rpc_path:
|
|
256
|
+
rpc_path: SPINE_RPC_PATH
|
|
187
257
|
});
|
|
188
|
-
const
|
|
258
|
+
const bystander_ws = await open_bystander(fixture);
|
|
189
259
|
try {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
260
|
+
const revokee_ws = await open_ws(revokee);
|
|
261
|
+
try {
|
|
262
|
+
const revoked = await rpc(fixture, 'role_grant_revoke', { actor_id: revokee.actor.id, role_grant_id, reason: 'cleanup' }, fixture.create_session_headers());
|
|
263
|
+
assert.isTrue(revoked.ok, `revoke failed: ${JSON.stringify(revoked)}`);
|
|
264
|
+
const is_revoke_frame = is_notification_with(ROLE_GRANT_REVOKE_NOTIFICATION_METHOD, (p) => p.role_grant_id === role_grant_id);
|
|
265
|
+
// Flat params — guards the no-`offer`-wrapper, `revoked_by`-omitted
|
|
266
|
+
// shape against drift.
|
|
267
|
+
const assert_flat_revoke = (frame) => {
|
|
268
|
+
const params = RoleGrantRevokeParams.parse(frame.params);
|
|
269
|
+
assert.strictEqual(params.role_grant_id, role_grant_id);
|
|
270
|
+
assert.strictEqual(params.role, ROLE_ADMIN);
|
|
271
|
+
assert.strictEqual(params.reason, 'cleanup');
|
|
272
|
+
assert.notProperty(frame.params, 'revoked_by');
|
|
273
|
+
};
|
|
274
|
+
if (revoke_closes_the_socket) {
|
|
275
|
+
// the socket lost the role its endpoint requires; the revoke
|
|
276
|
+
// notification may land just ahead of the close or not at all,
|
|
277
|
+
// and is held to the same shape when it does
|
|
278
|
+
const closed = await revokee_ws.wait_for_close(5000);
|
|
279
|
+
assert.ok(closed, 'the gated socket did not close after the revoke');
|
|
280
|
+
assert.strictEqual(revokee_ws.close_code, WS_CLOSE_PERMISSION_REVOKED);
|
|
281
|
+
const early = revokee_ws.messages.find(is_revoke_frame);
|
|
282
|
+
if (early)
|
|
283
|
+
assert_flat_revoke(early);
|
|
284
|
+
await assert_bystander_heard_nothing(bystander_ws);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
assert_flat_revoke(await revokee_ws.wait_for(is_revoke_frame, 5000));
|
|
288
|
+
await assert_bystander_heard_nothing(bystander_ws);
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
await revokee_ws.close();
|
|
213
292
|
}
|
|
214
|
-
assert_flat_revoke(await revokee_ws.wait_for(is_revoke_frame, 5000));
|
|
215
293
|
}
|
|
216
294
|
finally {
|
|
217
|
-
await
|
|
295
|
+
await bystander_ws.close();
|
|
218
296
|
}
|
|
219
297
|
});
|
|
220
298
|
test_if(capabilities.ws, 'accept of a sibling delivers role_grant_offer_supersede to the other grantor WS', async () => {
|
|
@@ -229,20 +307,27 @@ export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
|
229
307
|
const grantor2_offer = await create_pending_offer(fixture, recipient.account.id, grantor2.create_session_headers());
|
|
230
308
|
// Open grantor 1 (keeper) — its offer is the one that gets superseded
|
|
231
309
|
// when the recipient accepts grantor 2's offer.
|
|
232
|
-
const
|
|
310
|
+
const bystander_ws = await open_bystander(fixture);
|
|
233
311
|
try {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
312
|
+
const grantor1_ws = await open_ws(fixture);
|
|
313
|
+
try {
|
|
314
|
+
const accepted = await rpc(fixture, 'role_grant_offer_accept', { offer_id: grantor2_offer.id }, recipient.create_session_headers());
|
|
315
|
+
assert.isTrue(accepted.ok, `accept failed: ${JSON.stringify(accepted)}`);
|
|
316
|
+
const frame = await grantor1_ws.wait_for(is_notification_with(ROLE_GRANT_OFFER_SUPERSEDE_NOTIFICATION_METHOD, (p) => p.offer.id === keeper_offer.id), 5000);
|
|
317
|
+
const params = RoleGrantOfferSupersedeParams.parse(frame.params);
|
|
318
|
+
assert.strictEqual(params.offer.id, keeper_offer.id);
|
|
319
|
+
assert.strictEqual(params.reason, 'sibling_accepted');
|
|
320
|
+
// cause_id points at the accepted sibling (grantor 2's offer).
|
|
321
|
+
assert.strictEqual(params.cause_id, grantor2_offer.id);
|
|
322
|
+
assert.isNotNull(params.offer.superseded_at);
|
|
323
|
+
await assert_bystander_heard_nothing(bystander_ws);
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
await grantor1_ws.close();
|
|
327
|
+
}
|
|
243
328
|
}
|
|
244
329
|
finally {
|
|
245
|
-
await
|
|
330
|
+
await bystander_ws.close();
|
|
246
331
|
}
|
|
247
332
|
});
|
|
248
333
|
test_if(capabilities.ws, 'revoke supersedes a pending sibling offer and notifies its grantor WS', async () => {
|
|
@@ -250,26 +335,33 @@ export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
|
250
335
|
const grantor = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
251
336
|
const revokee = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
252
337
|
const role_grant_id = await create_active_role_grant(fixture, revokee, {
|
|
253
|
-
rpc_path:
|
|
338
|
+
rpc_path: SPINE_RPC_PATH
|
|
254
339
|
});
|
|
255
340
|
// A *pending* sibling offer for the same (account, role) — this is what
|
|
256
341
|
// the revoke cascade supersedes.
|
|
257
342
|
const sibling_offer = await create_pending_offer(fixture, revokee.account.id, grantor.create_session_headers());
|
|
258
343
|
// Watch the sibling grantor's socket for the supersede push.
|
|
259
|
-
const
|
|
344
|
+
const bystander_ws = await open_bystander(fixture);
|
|
260
345
|
try {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
346
|
+
const grantor_ws = await open_ws(grantor);
|
|
347
|
+
try {
|
|
348
|
+
const revoked = await rpc(fixture, 'role_grant_revoke', { actor_id: revokee.actor.id, role_grant_id }, fixture.create_session_headers());
|
|
349
|
+
assert.isTrue(revoked.ok, `revoke failed: ${JSON.stringify(revoked)}`);
|
|
350
|
+
const frame = await grantor_ws.wait_for(is_notification_with(ROLE_GRANT_OFFER_SUPERSEDE_NOTIFICATION_METHOD, (p) => p.offer.id === sibling_offer.id), 5000);
|
|
351
|
+
const params = RoleGrantOfferSupersedeParams.parse(frame.params);
|
|
352
|
+
assert.strictEqual(params.offer.id, sibling_offer.id);
|
|
353
|
+
assert.strictEqual(params.reason, 'role_grant_revoked');
|
|
354
|
+
// cause_id points at the revoked role_grant.
|
|
355
|
+
assert.strictEqual(params.cause_id, role_grant_id);
|
|
356
|
+
assert.isNotNull(params.offer.superseded_at);
|
|
357
|
+
await assert_bystander_heard_nothing(bystander_ws);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
await grantor_ws.close();
|
|
361
|
+
}
|
|
270
362
|
}
|
|
271
363
|
finally {
|
|
272
|
-
await
|
|
364
|
+
await bystander_ws.close();
|
|
273
365
|
}
|
|
274
366
|
});
|
|
275
367
|
});
|
|
@@ -6,6 +6,19 @@ export interface RoleGrantParticipationCrossTestOptions {
|
|
|
6
6
|
readonly setup_test: SetupTest;
|
|
7
7
|
/** RPC endpoint path. Default `/api/rpc`. */
|
|
8
8
|
readonly rpc_path?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The role conferred — one the backend's registry holds with the `admin`
|
|
11
|
+
* grant path. Default `SPINE_PARTICIPANT_ROLE`, the spine's own; a backend
|
|
12
|
+
* with only the builtin roles passes `ROLE_ADMIN`.
|
|
13
|
+
*/
|
|
14
|
+
readonly role?: string;
|
|
15
|
+
/**
|
|
16
|
+
* A scope kind `role` applies to, for the scoped-assign case; `null` skips
|
|
17
|
+
* that case. Default `'space'` (the spine participant's), or `null` for a
|
|
18
|
+
* builtin role, which is global-only (a scoped builtin is refused
|
|
19
|
+
* `role_grant_builtin_scoped`).
|
|
20
|
+
*/
|
|
21
|
+
readonly scope_kind?: string | null;
|
|
9
22
|
}
|
|
10
23
|
export declare const describe_role_grant_participation_cross_tests: (options: RoleGrantParticipationCrossTestOptions) => void;
|
|
11
24
|
//# sourceMappingURL=role_grant_participation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role_grant_participation.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_participation.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"role_grant_participation.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_participation.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2D9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,0EAA0E;AAC1E,MAAM,WAAW,sCAAsC;IACtD,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,eAAO,MAAM,6CAA6C,GACzD,SAAS,sCAAsC,KAC7C,IA6QF,CAAC"}
|
|
@@ -36,50 +36,61 @@ import '../assert_dev_env.js';
|
|
|
36
36
|
* The `participant` role is registered admin-grantable on both spines
|
|
37
37
|
* (`spine_roles` / the Rust stub's `RoleRegistry`), so the suite is ungated;
|
|
38
38
|
* every spine mounts the standard RPC surface it drives.
|
|
39
|
+
* The role is the `role` option, `participant` by default: a backend without
|
|
40
|
+
* it passes another admin-grantable role (`ROLE_ADMIN` for one with only the
|
|
41
|
+
* builtins, which skips the scoped case — builtins are global-only).
|
|
39
42
|
*
|
|
40
43
|
* @module
|
|
41
44
|
*/
|
|
42
45
|
import { describe, test, assert } from 'vitest';
|
|
46
|
+
import { is_builtin_role } from '../../auth/role_schema.js';
|
|
43
47
|
import { rpc_call } from '../rpc_helpers.js';
|
|
44
|
-
import { AuditLogListOutput } from '../../auth/admin_action_specs.js';
|
|
45
48
|
import { ERROR_ROLE_GRANT_OFFER_SELF_TARGET, ERROR_ROLE_GRANT_OFFER_TERMINAL } from '../../auth/role_grant_offer_action_specs.js';
|
|
46
49
|
import { JSONRPC_INVALID_PARAMS, JSONRPC_INVALID_REQUEST } from '../../http/jsonrpc.js';
|
|
50
|
+
import { list_audit_events } from './audit_cross_helpers.js';
|
|
51
|
+
import { test_if } from './capabilities.js';
|
|
47
52
|
import { SPINE_PARTICIPANT_ROLE, SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
48
53
|
export const describe_role_grant_participation_cross_tests = (options) => {
|
|
49
54
|
const { setup_test } = options;
|
|
50
55
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
56
|
+
const role = options.role ?? SPINE_PARTICIPANT_ROLE;
|
|
57
|
+
const scope_kind = options.scope_kind === undefined
|
|
58
|
+
? is_builtin_role(role)
|
|
59
|
+
? null
|
|
60
|
+
: 'space'
|
|
61
|
+
: options.scope_kind;
|
|
51
62
|
const rpc = (transport, method, params, headers) => rpc_call({ app: transport, path: rpc_path, method, params, headers });
|
|
52
63
|
describe('role-gated participation conferral (success paths)', () => {
|
|
53
|
-
test(
|
|
64
|
+
test(`admin assigns the ${role} app-role to a fresh account (idempotent)`, async () => {
|
|
54
65
|
const fixture = await setup_test();
|
|
55
66
|
const recipient = await fixture.create_account({ username: 'participation_assignee' });
|
|
56
67
|
// The keeper holds ROLE_ADMIN (seeded via extra_keeper_roles), so it
|
|
57
68
|
// clears the dispatcher admin gate; `participant` is admin-grantable, so
|
|
58
69
|
// it clears the grant-path gate; the recipient is single-actor, so the
|
|
59
70
|
// target actor resolves without `to_actor_id`.
|
|
60
|
-
const first = await rpc(fixture.transport, 'role_grant_assign', { to_account_id: recipient.account.id, role
|
|
71
|
+
const first = await rpc(fixture.transport, 'role_grant_assign', { to_account_id: recipient.account.id, role }, fixture.create_session_headers());
|
|
61
72
|
assert.ok(first.ok, `assign must succeed: ${JSON.stringify(first)}`);
|
|
62
73
|
const assigned = first.result;
|
|
63
74
|
assert.strictEqual(assigned.ok, true);
|
|
64
75
|
assert.ok(typeof assigned.role_grant_id === 'string' && assigned.role_grant_id.length > 0, 'assign returns the new role_grant id');
|
|
65
76
|
// Idempotent — re-assigning the active grant returns the existing id.
|
|
66
|
-
const second = await rpc(fixture.transport, 'role_grant_assign', { to_account_id: recipient.account.id, role
|
|
77
|
+
const second = await rpc(fixture.transport, 'role_grant_assign', { to_account_id: recipient.account.id, role }, fixture.create_session_headers());
|
|
67
78
|
assert.ok(second.ok, `re-assign must succeed: ${JSON.stringify(second)}`);
|
|
68
79
|
assert.strictEqual(second.result.role_grant_id, assigned.role_grant_id, 're-assigning an active grant returns the existing role_grant id');
|
|
69
80
|
});
|
|
70
|
-
|
|
81
|
+
test_if(scope_kind !== null, `admin assigns a scoped ${role} grant that round-trips scope_kind`, async () => {
|
|
71
82
|
const fixture = await setup_test();
|
|
72
83
|
const recipient = await fixture.create_account({ username: 'participation_scoped' });
|
|
73
84
|
const scope_id = '33333333-3333-4333-8333-333333333333';
|
|
74
85
|
const scoped = await rpc(fixture.transport, 'role_grant_assign', {
|
|
75
86
|
to_account_id: recipient.account.id,
|
|
76
|
-
role
|
|
77
|
-
scope_kind
|
|
87
|
+
role,
|
|
88
|
+
scope_kind,
|
|
78
89
|
scope_id
|
|
79
90
|
}, fixture.create_session_headers());
|
|
80
91
|
assert.ok(scoped.ok, `scoped assign must succeed: ${JSON.stringify(scoped)}`);
|
|
81
92
|
const scoped_id = scoped.result.role_grant_id;
|
|
82
|
-
const global = await rpc(fixture.transport, 'role_grant_assign', { to_account_id: recipient.account.id, role
|
|
93
|
+
const global = await rpc(fixture.transport, 'role_grant_assign', { to_account_id: recipient.account.id, role }, fixture.create_session_headers());
|
|
83
94
|
assert.ok(global.ok, `global assign must succeed: ${JSON.stringify(global)}`);
|
|
84
95
|
const global_id = global.result.role_grant_id;
|
|
85
96
|
assert.notStrictEqual(global_id, scoped_id, 'the global grant is a distinct row');
|
|
@@ -90,21 +101,21 @@ export const describe_role_grant_participation_cross_tests = (options) => {
|
|
|
90
101
|
const by_id = new Map(entry.role_grants.map((g) => [g.id, g]));
|
|
91
102
|
const scoped_row = by_id.get(scoped_id);
|
|
92
103
|
assert.ok(scoped_row, 'the scoped grant is listed');
|
|
93
|
-
assert.strictEqual(scoped_row.role,
|
|
94
|
-
assert.strictEqual(scoped_row.scope_kind,
|
|
104
|
+
assert.strictEqual(scoped_row.role, role);
|
|
105
|
+
assert.strictEqual(scoped_row.scope_kind, scope_kind, 'scope_kind round-trips');
|
|
95
106
|
assert.strictEqual(scoped_row.scope_id, scope_id, 'scope_id round-trips');
|
|
96
107
|
const global_row = by_id.get(global_id);
|
|
97
108
|
assert.ok(global_row, 'the global grant is listed');
|
|
98
109
|
assert.strictEqual(global_row.scope_kind, null);
|
|
99
110
|
assert.strictEqual(global_row.scope_id, null);
|
|
100
111
|
});
|
|
101
|
-
test(
|
|
112
|
+
test(`admin offers the ${role} app-role and the recipient accepts`, async () => {
|
|
102
113
|
const fixture = await setup_test();
|
|
103
114
|
const recipient = await fixture.create_account({ username: 'participation_offeree' });
|
|
104
115
|
// Admin-only conferral via the consent flow: the keeper (admin) offers
|
|
105
116
|
// the app-role, proving the widened grant-path gate admits it through
|
|
106
117
|
// the offer path too.
|
|
107
|
-
const created = await rpc(fixture.transport, 'role_grant_offer_create', { to_account_id: recipient.account.id, role
|
|
118
|
+
const created = await rpc(fixture.transport, 'role_grant_offer_create', { to_account_id: recipient.account.id, role }, fixture.create_session_headers());
|
|
108
119
|
assert.ok(created.ok, `offer create must succeed: ${JSON.stringify(created)}`);
|
|
109
120
|
const offer_id = created.result.offer.id;
|
|
110
121
|
// The recipient accepts → a role_grant lands.
|
|
@@ -112,9 +123,9 @@ export const describe_role_grant_participation_cross_tests = (options) => {
|
|
|
112
123
|
assert.ok(accepted.ok, `accept must succeed: ${JSON.stringify(accepted)}`);
|
|
113
124
|
assert.ok(typeof accepted.result.role_grant_id === 'string', 'accept lands a role_grant for the app-role');
|
|
114
125
|
});
|
|
115
|
-
/** Offer `
|
|
126
|
+
/** Offer `role` to `recipient` as the keeper and accept it; returns the ids. */
|
|
116
127
|
const offer_and_accept = async (fixture, recipient) => {
|
|
117
|
-
const created = await rpc(fixture.transport, 'role_grant_offer_create', { to_account_id: recipient.account.id, role
|
|
128
|
+
const created = await rpc(fixture.transport, 'role_grant_offer_create', { to_account_id: recipient.account.id, role }, fixture.create_session_headers());
|
|
118
129
|
assert.ok(created.ok, `offer create must succeed: ${JSON.stringify(created)}`);
|
|
119
130
|
const offer_id = created.result.offer.id;
|
|
120
131
|
const accepted = await rpc(fixture.fresh_transport(), 'role_grant_offer_accept', { offer_id }, recipient.create_session_headers());
|
|
@@ -122,12 +133,12 @@ export const describe_role_grant_participation_cross_tests = (options) => {
|
|
|
122
133
|
const { role_grant_id } = accepted.result;
|
|
123
134
|
return { offer_id, role_grant_id };
|
|
124
135
|
};
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
136
|
+
/**
|
|
137
|
+
* How many `event_type` audit rows the keeper can list, behind the drain
|
|
138
|
+
* barrier — without it a replay's late row could land after the count,
|
|
139
|
+
* and the no-new-row assertions would pass vacuously.
|
|
140
|
+
*/
|
|
141
|
+
const count_audit_rows = async (fixture, event_type) => (await list_audit_events(fixture, { event_type }, rpc_path)).length;
|
|
131
142
|
test('re-accepting an accepted offer answers the same grant and writes no audit row', async () => {
|
|
132
143
|
const fixture = await setup_test();
|
|
133
144
|
const recipient = await fixture.create_account({ username: 'participation_replayer' });
|
|
@@ -157,7 +168,7 @@ export const describe_role_grant_participation_cross_tests = (options) => {
|
|
|
157
168
|
const fixture = await setup_test();
|
|
158
169
|
const own_id = fixture.account.id;
|
|
159
170
|
for (const to_account_id of [own_id, own_id.toUpperCase()]) {
|
|
160
|
-
const res = await rpc(fixture.transport, 'role_grant_offer_create', { to_account_id, role
|
|
171
|
+
const res = await rpc(fixture.transport, 'role_grant_offer_create', { to_account_id, role }, fixture.create_session_headers());
|
|
161
172
|
assert.ok(!res.ok, `a self-offer must be refused (${to_account_id}): ${JSON.stringify(res)}`);
|
|
162
173
|
assert.strictEqual(res.status, 400, to_account_id);
|
|
163
174
|
assert.strictEqual(res.error.code, JSONRPC_INVALID_PARAMS, to_account_id);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role_grant_role_grammar.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_role_grammar.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"role_grant_role_grammar.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_role_grammar.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,8EAA8E;AAC9E,MAAM,MAAM,oCAAoC,GAAG,wBAAwB,CAAC;AAoC5E,eAAO,MAAM,4CAA4C,GACxD,SAAS,oCAAoC,KAC3C,IAyFF,CAAC"}
|