@fuzdev/fuz_app 0.146.1 → 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/CLAUDE.md +1 -1
- package/dist/auth/bootstrap_account.d.ts +2 -1
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +17 -6
- 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/keyring.d.ts +35 -38
- package/dist/auth/keyring.d.ts.map +1 -1
- package/dist/auth/keyring.js +43 -50
- package/dist/auth/password_argon2.d.ts.map +1 -1
- package/dist/auth/password_argon2.js +11 -8
- 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/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +5 -2
- package/dist/db/CLAUDE.md +18 -16
- package/dist/db/cell_queries.d.ts +3 -2
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +3 -2
- package/dist/db/fact_disk_storage.d.ts +15 -15
- package/dist/db/fact_disk_storage.d.ts.map +1 -1
- package/dist/db/fact_disk_storage.js +22 -28
- package/dist/db/fact_queries.d.ts +2 -3
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +2 -3
- package/dist/db/fact_store.d.ts +20 -18
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/fact_store.js +30 -44
- package/dist/db/file_fact_url.d.ts +19 -4
- package/dist/db/file_fact_url.d.ts.map +1 -1
- package/dist/db/file_fact_url.js +19 -4
- 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/server/env.d.ts.map +1 -1
- package/dist/server/env.js +2 -2
- package/dist/server/serve_fact_route.d.ts +9 -5
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +15 -7
- package/dist/testing/CLAUDE.md +737 -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/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +2 -2
- 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/bootstrap_backend.d.ts +11 -1
- package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -1
- package/dist/testing/cross_backend/bootstrap_backend.js +2 -1
- 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 +5 -0
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +41 -7
- 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.d.ts +19 -0
- package/dist/testing/cross_backend/session_cookie_codec.d.ts.map +1 -0
- package/dist/testing/cross_backend/session_cookie_codec.js +128 -0
- 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 +10 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +21 -12
- 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 +10 -8
- 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
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for the **`cell_moderate` verb** and the
|
|
4
|
+
* moderation read gate, over the directory-model test authorizer both spines
|
|
5
|
+
* mount (`test_cell_gated_create_authorize.ts`) — the sibling of
|
|
6
|
+
* `describe_cell_gated_create_cross_tests` in `cell_gated_create.ts`, whose
|
|
7
|
+
* authorizer is what produces a `pending` contribution. The suite's cases are
|
|
8
|
+
* listed on `describe_cell_moderate_cross_tests`.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { describe, assert } from 'vitest';
|
|
13
|
+
import { fractional_index_between } from '@fuzdev/fuz_util/fractional_index.ts';
|
|
14
|
+
import { CellCloneOutput, CellCreateOutput, CellGetOutput, CellListOutput, CellModerateOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
|
|
15
|
+
import { CellItemInsertOutput, CellItemListOutput } from '../../auth/cell_item_action_specs.js';
|
|
16
|
+
import { CellGrantCreateOutput } from '../../auth/cell_grant_action_specs.js';
|
|
17
|
+
import { CellFieldListOutput, CellFieldSetOutput } from '../../auth/cell_field_action_specs.js';
|
|
18
|
+
import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
|
|
19
|
+
import { test_if } from './capabilities.js';
|
|
20
|
+
import { cross_rpc_call, error_reason, expect_output } from './cross_rpc.js';
|
|
21
|
+
import { cross_put_fact } from './fact_serving.js';
|
|
22
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
23
|
+
import { PARTICIPATION_ROLE, SPACE_CELL_KIND, TEST_SPACE_POLICY } from './test_cell_gated_create_authorize.js';
|
|
24
|
+
/**
|
|
25
|
+
* Cross-backend parity for the **`cell_moderate` verb** (the `pending →
|
|
26
|
+
* approved | rejected` transition) — root-authority-gated.
|
|
27
|
+
*
|
|
28
|
+
* Builds a moderated public space (admin) + a `participant`'s `post` (born
|
|
29
|
+
* `pending` + private under the `moderation_required: true` policy), then
|
|
30
|
+
* proves both spines agree:
|
|
31
|
+
*
|
|
32
|
+
* - **a root manager (admin) approves** → `moderation: 'approved'` +
|
|
33
|
+
* `visibility: 'public'` (the contribution goes live).
|
|
34
|
+
* - **the author cannot self-approve** → `403 cell_moderate_forbidden` (the
|
|
35
|
+
* author can *view* their own pending cell, so they reach the gate, and are
|
|
36
|
+
* denied — moderation authority is over the governing root, not the
|
|
37
|
+
* contribution).
|
|
38
|
+
* - **a non-viewer is 404-masked** → a stranger who can't see the pending
|
|
39
|
+
* contribution gets `cell_not_found`, never learns it exists.
|
|
40
|
+
* - **reject** → `moderation: 'rejected'`, visibility stays private.
|
|
41
|
+
* - **a cell with no governing root** → `400 cell_not_a_contribution` for any
|
|
42
|
+
* viewer (owner, admin), the cell left unmoderated; a non-viewer gets the
|
|
43
|
+
* `cell_not_found` mask first.
|
|
44
|
+
*
|
|
45
|
+
* And that moderation gates reads — a cell's effective audience is its
|
|
46
|
+
* visibility or grants **and** its moderation admission (unmoderated or
|
|
47
|
+
* `approved`), so a pending or rejected contribution reaches no one past its
|
|
48
|
+
* author and admins:
|
|
49
|
+
*
|
|
50
|
+
* - **an author's self-flip to public admits nobody** — a pending post its
|
|
51
|
+
* author makes public stays 404 to anon and a fresh non-admin on `cell_get`,
|
|
52
|
+
* and out of their `cell_list`; the author and admin still read it.
|
|
53
|
+
* - **rejection holds a public post back** — pending → rejected → public, and
|
|
54
|
+
* approved → rejected (approval made it public, rejection leaves visibility
|
|
55
|
+
* alone), both 404 past the author; a grant made while the post was approved
|
|
56
|
+
* stops admitting once it is rejected, on `cell_get` and `shared_with: 'me'`.
|
|
57
|
+
* - **an approved post is live** — the control.
|
|
58
|
+
* - **relation reads follow** — a pending post in a public collection is
|
|
59
|
+
* omitted from anon `cell_item_list`, beside a live sibling that isn't; at
|
|
60
|
+
* the far end of all four relation lists, a pending post is hidden from
|
|
61
|
+
* anon even flipped public, and a rejected one from its grant holder.
|
|
62
|
+
* - **fact serving follows** — a pending post's fact is 404 to anon, even
|
|
63
|
+
* with the post flipped public (also gated on `capabilities.fact_serving`).
|
|
64
|
+
*
|
|
65
|
+
* And the steward arm — the owner of a **non-admin** root (a parentless
|
|
66
|
+
* non-`space` kind carrying the same policy, which the test authorizer leaves
|
|
67
|
+
* open) reads the pending posts under it and moderates them:
|
|
68
|
+
*
|
|
69
|
+
* - **the queue** — `cell_list({root_id, moderation: 'pending'})` and
|
|
70
|
+
* `cell_get` (with `can_edit` / `can_grant` false) → 200; a stranger 404s.
|
|
71
|
+
* - **moderation** — the root owner approves → 200; the author 403, a
|
|
72
|
+
* stranger 404.
|
|
73
|
+
* - **the arm stops there** — the root owner's `cell_clone` of the pending
|
|
74
|
+
* post and a contribution under it are both 404.
|
|
75
|
+
* - **fact serving** — the root owner fetches a pending post's fact → 200,
|
|
76
|
+
* anon 404 (also gated on `capabilities.fact_serving`).
|
|
77
|
+
*
|
|
78
|
+
* And that `cell_grant_create` refuses a pending contribution — **403**
|
|
79
|
+
* `cell_grant_unapproved` for its author and an admin, actor- and role-shaped,
|
|
80
|
+
* while a non-manager keeps the **404** mask; the same grant lands once the
|
|
81
|
+
* post is approved.
|
|
82
|
+
*
|
|
83
|
+
* Gated on `capabilities.cell_gated_create` — a `pending` contribution only
|
|
84
|
+
* exists when the directory authorizer is mounted (reference spine binaries).
|
|
85
|
+
*/
|
|
86
|
+
export const describe_cell_moderate_cross_tests = (options) => {
|
|
87
|
+
const { setup_test, capabilities } = options;
|
|
88
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
89
|
+
// Seed `content` as an embedded fact the author holds (as their upload
|
|
90
|
+
// would) — a non-admin cell may reference only a fact its writer holds —
|
|
91
|
+
// and return the post `data` referencing it, `{}` for no fact.
|
|
92
|
+
const author_post_data = async (fixture, author, post_fact_content) => {
|
|
93
|
+
if (post_fact_content === undefined)
|
|
94
|
+
return { post_data: {}, hash: null };
|
|
95
|
+
const hash = await cross_put_fact(fixture, rpc_path, {
|
|
96
|
+
content: post_fact_content,
|
|
97
|
+
holder: author.actor.id
|
|
98
|
+
});
|
|
99
|
+
return { post_data: { cover: hash }, hash };
|
|
100
|
+
};
|
|
101
|
+
// Build an admin-owned public space (moderated `post` policy) + a
|
|
102
|
+
// participant's pending post under it, optionally carrying a fact the
|
|
103
|
+
// author put.
|
|
104
|
+
const setup_pending_post_on = async (fixture, post_fact_content) => {
|
|
105
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: TEST_SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
|
|
106
|
+
const author = await fixture.create_account({
|
|
107
|
+
username: 'moderate_author',
|
|
108
|
+
roles: [PARTICIPATION_ROLE]
|
|
109
|
+
});
|
|
110
|
+
const { post_data, hash } = await author_post_data(fixture, author, post_fact_content);
|
|
111
|
+
const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: post_data, parent_id: space.cell.id }, author.create_session_headers()), CellCreateOutput);
|
|
112
|
+
return { fixture, space, author, post, hash };
|
|
113
|
+
};
|
|
114
|
+
const setup_pending_post = async () => setup_pending_post_on(await setup_test());
|
|
115
|
+
// A non-admin's policy root (a parentless non-`space` kind stays open under
|
|
116
|
+
// the test authorizer, and contributions read the policy off any root) + a
|
|
117
|
+
// participant's pending post under it, optionally carrying a fact the author
|
|
118
|
+
// put.
|
|
119
|
+
const setup_stewarded_post_on = async (fixture, post_fact_content) => {
|
|
120
|
+
const root_owner = await fixture.create_account({ username: 'steward_root_owner' });
|
|
121
|
+
const root = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'forum', data: TEST_SPACE_POLICY, visibility: 'public' }, root_owner.create_session_headers()), CellCreateOutput);
|
|
122
|
+
const author = await fixture.create_account({
|
|
123
|
+
username: 'steward_author',
|
|
124
|
+
roles: [PARTICIPATION_ROLE]
|
|
125
|
+
});
|
|
126
|
+
const { post_data, hash } = await author_post_data(fixture, author, post_fact_content);
|
|
127
|
+
const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: post_data, parent_id: root.cell.id }, author.create_session_headers()), CellCreateOutput);
|
|
128
|
+
assert.strictEqual(post.cell.moderation, 'pending', "the root's policy gates the post");
|
|
129
|
+
return { fixture, root, root_owner, author, post, hash };
|
|
130
|
+
};
|
|
131
|
+
const setup_stewarded_post = async () => setup_stewarded_post_on(await setup_test());
|
|
132
|
+
/** The status of a fact GET — `null` headers = anon. */
|
|
133
|
+
const fact_status = async (fixture, path, headers) => {
|
|
134
|
+
const transport = headers === null ? fixture.fresh_transport({ origin: null }) : fixture.transport;
|
|
135
|
+
const res = await transport(path, { method: 'GET', headers: headers ?? {} });
|
|
136
|
+
await res.text();
|
|
137
|
+
return res.status;
|
|
138
|
+
};
|
|
139
|
+
/** `'ok'`, or the refusal reason, for one caller's RPC — `null` headers = anon. */
|
|
140
|
+
const outcome = async (fixture, method, params, headers) => {
|
|
141
|
+
const r = headers === null
|
|
142
|
+
? await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, method, params, {})
|
|
143
|
+
: await cross_rpc_call(fixture.transport, rpc_path, method, params, headers);
|
|
144
|
+
return r.ok ? 'ok' : error_reason(r);
|
|
145
|
+
};
|
|
146
|
+
/** The ids a caller's `cell_list` returns — `null` headers = anon. */
|
|
147
|
+
const listed_ids = async (fixture, params, headers) => {
|
|
148
|
+
const r = headers === null
|
|
149
|
+
? await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, 'cell_list', params, {})
|
|
150
|
+
: await cross_rpc_call(fixture.transport, rpc_path, 'cell_list', params, headers);
|
|
151
|
+
return expect_output(r, CellListOutput).cells.map((c) => c.id);
|
|
152
|
+
};
|
|
153
|
+
const moderate = async (fixture, cell_id, moderation, headers = fixture.create_session_headers()) => expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id, moderation }, headers), CellModerateOutput).cell;
|
|
154
|
+
const set_visibility = async (fixture, headers, cell_id, visibility) => expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_update', { cell_id, visibility }, headers), CellUpdateOutput).cell;
|
|
155
|
+
describe('cell_moderate verb parity (root-authority-gated)', () => {
|
|
156
|
+
test_if(capabilities.cell_gated_create, 'a root manager (admin) approves a pending post → approved + public', async () => {
|
|
157
|
+
const { fixture, post } = await setup_pending_post();
|
|
158
|
+
assert.strictEqual(post.cell.moderation, 'pending', 'the post is born pending');
|
|
159
|
+
const moderated = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: post.cell.id, moderation: 'approved' }, fixture.create_session_headers() // keeper = admin = root manager
|
|
160
|
+
), CellModerateOutput);
|
|
161
|
+
assert.strictEqual(moderated.cell.moderation, 'approved', 'approval sets the marker');
|
|
162
|
+
assert.strictEqual(moderated.cell.visibility, 'public', 'approval publishes the post');
|
|
163
|
+
});
|
|
164
|
+
test_if(capabilities.cell_gated_create, 'the author (non-manager) cannot moderate their own pending post → 403', async () => {
|
|
165
|
+
const { fixture, author, post } = await setup_pending_post();
|
|
166
|
+
const denied = await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: post.cell.id, moderation: 'approved' }, author.create_session_headers());
|
|
167
|
+
assert.ok(!denied.ok, 'the author must not self-approve');
|
|
168
|
+
assert.strictEqual(error_reason(denied), 'cell_moderate_forbidden');
|
|
169
|
+
});
|
|
170
|
+
test_if(capabilities.cell_gated_create, 'a non-viewer is 404-masked from moderating a pending post', async () => {
|
|
171
|
+
const { fixture, post } = await setup_pending_post();
|
|
172
|
+
const stranger = await fixture.create_account({ username: 'moderate_stranger' });
|
|
173
|
+
const masked = await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: post.cell.id, moderation: 'approved' }, stranger.create_session_headers());
|
|
174
|
+
assert.ok(!masked.ok, 'a non-viewer must not moderate');
|
|
175
|
+
assert.strictEqual(error_reason(masked), 'cell_not_found', 'a non-viewer never learns the pending post exists');
|
|
176
|
+
});
|
|
177
|
+
test_if(capabilities.cell_gated_create, 'a root manager rejects a pending post → rejected, stays private', async () => {
|
|
178
|
+
const { fixture, post } = await setup_pending_post();
|
|
179
|
+
const moderated = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: post.cell.id, moderation: 'rejected' }, fixture.create_session_headers()), CellModerateOutput);
|
|
180
|
+
assert.strictEqual(moderated.cell.moderation, 'rejected', 'rejection sets the marker');
|
|
181
|
+
assert.strictEqual(moderated.cell.visibility, 'private', 'rejection leaves the post private');
|
|
182
|
+
});
|
|
183
|
+
test_if(capabilities.cell_gated_create, 'moderating a cell with no governing root → 400 cell_not_a_contribution, after the view check', async () => {
|
|
184
|
+
const fixture = await setup_test();
|
|
185
|
+
const owner = await fixture.create_account({ username: 'moderate_root_owner' });
|
|
186
|
+
const owner_h = owner.create_session_headers();
|
|
187
|
+
const create = async (visibility) => expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'note', data: {}, visibility }, owner_h), CellCreateOutput).cell;
|
|
188
|
+
const pub = await create('public');
|
|
189
|
+
const priv = await create('private');
|
|
190
|
+
// a viewer of a parentless cell — its owner, an admin, anyone on a
|
|
191
|
+
// public one — reaches the root check and is refused there
|
|
192
|
+
for (const [label, cell_id, headers] of [
|
|
193
|
+
['the owner', priv.id, owner_h],
|
|
194
|
+
['an admin', priv.id, fixture.create_session_headers()],
|
|
195
|
+
['the owner, public', pub.id, owner_h]
|
|
196
|
+
]) {
|
|
197
|
+
const refused = await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id, moderation: 'approved' }, headers);
|
|
198
|
+
assert.ok(!refused.ok, `${label}: moderated a cell with no governing root`);
|
|
199
|
+
assert.strictEqual(refused.error?.code, JSONRPC_ERROR_CODES.invalid_params, `${label}: code`);
|
|
200
|
+
assert.strictEqual(error_reason(refused), 'cell_not_a_contribution', `${label}: reason`);
|
|
201
|
+
}
|
|
202
|
+
// a non-viewer meets the 404 mask first, never the shape refusal
|
|
203
|
+
const stranger = await fixture.create_account({ username: 'moderate_root_stranger' });
|
|
204
|
+
const masked = await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: priv.id, moderation: 'approved' }, stranger.create_session_headers());
|
|
205
|
+
assert.strictEqual(error_reason(masked), 'cell_not_found');
|
|
206
|
+
// the refusals left the cell unmoderated
|
|
207
|
+
const got = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_get', { id: priv.id }, owner_h), CellGetOutput);
|
|
208
|
+
assert.strictEqual(got.cell.moderation, null);
|
|
209
|
+
assert.strictEqual(got.cell.visibility, 'private');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe('moderation gates reads (parity)', () => {
|
|
213
|
+
test_if(capabilities.cell_gated_create, 'a pending post its author flips public stays hidden past the author and admins', async () => {
|
|
214
|
+
const { fixture, space, author, post } = await setup_pending_post();
|
|
215
|
+
const author_h = author.create_session_headers();
|
|
216
|
+
const flipped = await set_visibility(fixture, author_h, post.cell.id, 'public');
|
|
217
|
+
assert.strictEqual(flipped.visibility, 'public', 'the author manages their own post');
|
|
218
|
+
assert.strictEqual(flipped.moderation, 'pending', 'a visibility write leaves moderation');
|
|
219
|
+
const stranger = await fixture.create_account({ username: 'flip_stranger' });
|
|
220
|
+
const stranger_h = stranger.create_session_headers();
|
|
221
|
+
const get = { id: post.cell.id };
|
|
222
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, null), 'cell_not_found');
|
|
223
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, stranger_h), 'cell_not_found');
|
|
224
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, author_h), 'ok');
|
|
225
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, fixture.create_session_headers()), 'ok');
|
|
226
|
+
const feed = { root_id: space.cell.id };
|
|
227
|
+
assert.notInclude(await listed_ids(fixture, feed, null), post.cell.id, 'anon list');
|
|
228
|
+
assert.notInclude(await listed_ids(fixture, feed, stranger_h), post.cell.id, 'stranger');
|
|
229
|
+
assert.include(await listed_ids(fixture, feed, author_h), post.cell.id, 'author list');
|
|
230
|
+
assert.include(await listed_ids(fixture, feed, fixture.create_session_headers()), post.cell.id, 'admin list');
|
|
231
|
+
});
|
|
232
|
+
test_if(capabilities.cell_gated_create, 'pending → rejected → public stays hidden past the author', async () => {
|
|
233
|
+
const { fixture, author, post } = await setup_pending_post();
|
|
234
|
+
const author_h = author.create_session_headers();
|
|
235
|
+
assert.strictEqual((await moderate(fixture, post.cell.id, 'rejected')).moderation, 'rejected');
|
|
236
|
+
await set_visibility(fixture, author_h, post.cell.id, 'public');
|
|
237
|
+
const stranger = await fixture.create_account({ username: 'rejected_stranger' });
|
|
238
|
+
const get = { id: post.cell.id };
|
|
239
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, null), 'cell_not_found');
|
|
240
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, stranger.create_session_headers()), 'cell_not_found');
|
|
241
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, author_h), 'ok');
|
|
242
|
+
});
|
|
243
|
+
test_if(capabilities.cell_gated_create, 'approved → rejected stays hidden, and a grant made while approved stops admitting', async () => {
|
|
244
|
+
const { fixture, author, post } = await setup_pending_post();
|
|
245
|
+
const author_h = author.create_session_headers();
|
|
246
|
+
const friend = await fixture.create_account({ username: 'approved_friend' });
|
|
247
|
+
const friend_h = friend.create_session_headers();
|
|
248
|
+
const stranger = await fixture.create_account({ username: 'approved_stranger' });
|
|
249
|
+
const get = { id: post.cell.id };
|
|
250
|
+
const shared = { shared_with: 'me' };
|
|
251
|
+
// The control: approval publishes the post to everyone.
|
|
252
|
+
assert.strictEqual((await moderate(fixture, post.cell.id, 'approved')).visibility, 'public');
|
|
253
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, null), 'ok', 'approved is live');
|
|
254
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, stranger.create_session_headers()), 'ok');
|
|
255
|
+
// A grant made while approved admits on its own once the post is private.
|
|
256
|
+
expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_grant_create', {
|
|
257
|
+
cell_id: post.cell.id,
|
|
258
|
+
level: 'viewer',
|
|
259
|
+
principal: { kind: 'actor', actor_id: friend.actor.id }
|
|
260
|
+
}, author_h), CellGrantCreateOutput);
|
|
261
|
+
await set_visibility(fixture, author_h, post.cell.id, 'private');
|
|
262
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, friend_h), 'ok', 'grant admits');
|
|
263
|
+
assert.include(await listed_ids(fixture, shared, friend_h), post.cell.id);
|
|
264
|
+
// Rejection holds it back from the grant holder ...
|
|
265
|
+
assert.strictEqual((await moderate(fixture, post.cell.id, 'rejected')).moderation, 'rejected');
|
|
266
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, friend_h), 'cell_not_found');
|
|
267
|
+
assert.notInclude(await listed_ids(fixture, shared, friend_h), post.cell.id);
|
|
268
|
+
// ... and from everyone once public again; the author still reads it.
|
|
269
|
+
await set_visibility(fixture, author_h, post.cell.id, 'public');
|
|
270
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, null), 'cell_not_found');
|
|
271
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, stranger.create_session_headers()), 'cell_not_found');
|
|
272
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, friend_h), 'cell_not_found');
|
|
273
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, author_h), 'ok');
|
|
274
|
+
});
|
|
275
|
+
test_if(capabilities.cell_gated_create, 'approval leaves visibility to rejection: an approved-then-rejected post is still public, and hidden', async () => {
|
|
276
|
+
const { fixture, post } = await setup_pending_post();
|
|
277
|
+
await moderate(fixture, post.cell.id, 'approved');
|
|
278
|
+
const rejected = await moderate(fixture, post.cell.id, 'rejected');
|
|
279
|
+
assert.strictEqual(rejected.visibility, 'public', 'rejection does not reset visibility');
|
|
280
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', { id: post.cell.id }, null), 'cell_not_found');
|
|
281
|
+
});
|
|
282
|
+
test_if(capabilities.cell_gated_create, 'an editor grant made while approved admits no write once rejected; the author keeps the owner arm', async () => {
|
|
283
|
+
const { fixture, author, post } = await setup_pending_post();
|
|
284
|
+
const author_h = author.create_session_headers();
|
|
285
|
+
const editor = await fixture.create_account({ username: 'rejected_editor' });
|
|
286
|
+
const editor_h = editor.create_session_headers();
|
|
287
|
+
const other = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'note', data: {}, visibility: 'public' }, editor_h), CellCreateOutput);
|
|
288
|
+
await moderate(fixture, post.cell.id, 'approved');
|
|
289
|
+
expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_grant_create', {
|
|
290
|
+
cell_id: post.cell.id,
|
|
291
|
+
level: 'editor',
|
|
292
|
+
principal: { kind: 'actor', actor_id: editor.actor.id }
|
|
293
|
+
}, author_h), CellGrantCreateOutput);
|
|
294
|
+
const update = { cell_id: post.cell.id };
|
|
295
|
+
assert.strictEqual(await outcome(fixture, 'cell_update', update, editor_h), 'ok', 'the grant admits a write while approved');
|
|
296
|
+
await moderate(fixture, post.cell.id, 'rejected');
|
|
297
|
+
const writes = [
|
|
298
|
+
['cell_get', { id: post.cell.id }],
|
|
299
|
+
['cell_update', update],
|
|
300
|
+
['cell_field_set', { source_id: post.cell.id, name: 'link', target_id: other.cell.id }],
|
|
301
|
+
[
|
|
302
|
+
'cell_item_insert',
|
|
303
|
+
{ parent_id: post.cell.id, child_id: other.cell.id, position: 'a0' }
|
|
304
|
+
],
|
|
305
|
+
['cell_delete', { cell_id: post.cell.id }]
|
|
306
|
+
];
|
|
307
|
+
for (const [method, params] of writes) {
|
|
308
|
+
assert.strictEqual(await outcome(fixture, method, params, editor_h), 'cell_not_found', method);
|
|
309
|
+
}
|
|
310
|
+
assert.strictEqual(await outcome(fixture, 'cell_update', update, author_h), 'ok', 'author');
|
|
311
|
+
});
|
|
312
|
+
test_if(capabilities.cell_gated_create, 'a pending post in a public collection is omitted from anon cell_item_list', async () => {
|
|
313
|
+
const { fixture, space, author, post } = await setup_pending_post();
|
|
314
|
+
const author_h = author.create_session_headers();
|
|
315
|
+
// A live sibling under the same space: a `react` is born approved + public.
|
|
316
|
+
const react = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'react', data: {}, parent_id: space.cell.id, visibility: 'public' }, author_h), CellCreateOutput);
|
|
317
|
+
await set_visibility(fixture, author_h, post.cell.id, 'public');
|
|
318
|
+
const collection = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'collection', data: {}, visibility: 'public' }, author_h), CellCreateOutput);
|
|
319
|
+
const pos_a = fractional_index_between(null, null);
|
|
320
|
+
const pos_b = fractional_index_between(pos_a, null);
|
|
321
|
+
for (const [child_id, position] of [
|
|
322
|
+
[post.cell.id, pos_a],
|
|
323
|
+
[react.cell.id, pos_b]
|
|
324
|
+
]) {
|
|
325
|
+
expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_item_insert', { parent_id: collection.cell.id, child_id, position }, author_h), CellItemInsertOutput);
|
|
326
|
+
}
|
|
327
|
+
const items = async (headers) => {
|
|
328
|
+
const params = { parent_id: collection.cell.id };
|
|
329
|
+
const r = headers === null
|
|
330
|
+
? await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, 'cell_item_list', params, {})
|
|
331
|
+
: await cross_rpc_call(fixture.transport, rpc_path, 'cell_item_list', params, headers);
|
|
332
|
+
return expect_output(r, CellItemListOutput).items.map((i) => i.child_id);
|
|
333
|
+
};
|
|
334
|
+
assert.deepStrictEqual(await items(null), [react.cell.id], 'anon sees the live sibling only');
|
|
335
|
+
assert.deepStrictEqual(await items(author_h), [post.cell.id, react.cell.id]);
|
|
336
|
+
});
|
|
337
|
+
test_if(capabilities.cell_gated_create, 'every relation list hides a pending end, and a rejected one from its grant holder', async () => {
|
|
338
|
+
const { fixture, author, post } = await setup_pending_post();
|
|
339
|
+
const author_h = author.create_session_headers();
|
|
340
|
+
const holder = await fixture.create_account({ username: 'relation_grant_holder' });
|
|
341
|
+
const holder_h = holder.create_session_headers();
|
|
342
|
+
const create = async (kind) => expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind, data: {}, visibility: 'public' }, author_h), CellCreateOutput).cell.id;
|
|
343
|
+
const call = async (method, params) => cross_rpc_call(fixture.transport, rpc_path, method, params, author_h);
|
|
344
|
+
// The post at the far end of all four lists: a public source and
|
|
345
|
+
// collection point at it, and it points at a public target and child.
|
|
346
|
+
const source = await create('note');
|
|
347
|
+
const collection = await create('collection');
|
|
348
|
+
const target = await create('note');
|
|
349
|
+
const child = await create('note');
|
|
350
|
+
const post_id = post.cell.id;
|
|
351
|
+
expect_output(await call('cell_field_set', { source_id: source, name: 'post', target_id: post_id }), CellFieldSetOutput);
|
|
352
|
+
expect_output(await call('cell_item_insert', {
|
|
353
|
+
parent_id: collection,
|
|
354
|
+
child_id: post_id,
|
|
355
|
+
position: 'a0'
|
|
356
|
+
}), CellItemInsertOutput);
|
|
357
|
+
expect_output(await call('cell_field_set', { source_id: post_id, name: 'link', target_id: target }), CellFieldSetOutput);
|
|
358
|
+
expect_output(await call('cell_item_insert', { parent_id: post_id, child_id: child, position: 'a0' }), CellItemInsertOutput);
|
|
359
|
+
// Which lists show the post to a caller — `null` headers = anon.
|
|
360
|
+
const reach = async (headers) => {
|
|
361
|
+
const list = async (method, params) => headers === null
|
|
362
|
+
? cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, method, params, {})
|
|
363
|
+
: cross_rpc_call(fixture.transport, rpc_path, method, params, headers);
|
|
364
|
+
const forward_fields = expect_output(await list('cell_field_list', { source_id: source }), CellFieldListOutput).fields.map((f) => f.target_id);
|
|
365
|
+
const forward_items = expect_output(await list('cell_item_list', { parent_id: collection }), CellItemListOutput).items.map((i) => i.child_id);
|
|
366
|
+
const reverse_fields = expect_output(await list('cell_field_list', { target_id: target }), CellFieldListOutput).fields.map((f) => f.source_id);
|
|
367
|
+
const reverse_items = expect_output(await list('cell_item_list', { child_id: child }), CellItemListOutput).items.map((i) => i.parent_id);
|
|
368
|
+
return [forward_fields, forward_items, reverse_fields, reverse_items].map((ids) => ids.includes(post_id));
|
|
369
|
+
};
|
|
370
|
+
const everywhere = [true, true, true, true];
|
|
371
|
+
const nowhere = [false, false, false, false];
|
|
372
|
+
// pending, flipped public: the audience sees nothing, the author all
|
|
373
|
+
await set_visibility(fixture, author_h, post_id, 'public');
|
|
374
|
+
assert.deepStrictEqual(await reach(null), nowhere, 'anon, pending + public');
|
|
375
|
+
assert.deepStrictEqual(await reach(author_h), everywhere, 'author, pending + public');
|
|
376
|
+
// approved and granted, then private: the grant admits it
|
|
377
|
+
await moderate(fixture, post_id, 'approved');
|
|
378
|
+
expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_grant_create', {
|
|
379
|
+
cell_id: post_id,
|
|
380
|
+
level: 'viewer',
|
|
381
|
+
principal: { kind: 'actor', actor_id: holder.actor.id }
|
|
382
|
+
}, author_h), CellGrantCreateOutput);
|
|
383
|
+
await set_visibility(fixture, author_h, post_id, 'private');
|
|
384
|
+
assert.deepStrictEqual(await reach(holder_h), everywhere, 'grant holder, approved');
|
|
385
|
+
assert.deepStrictEqual(await reach(null), nowhere, 'anon, approved + private');
|
|
386
|
+
// rejected: the grant admits nothing, public or not
|
|
387
|
+
await moderate(fixture, post_id, 'rejected');
|
|
388
|
+
assert.deepStrictEqual(await reach(holder_h), nowhere, 'grant holder, rejected');
|
|
389
|
+
await set_visibility(fixture, author_h, post_id, 'public');
|
|
390
|
+
assert.deepStrictEqual(await reach(holder_h), nowhere, 'grant holder, rejected + public');
|
|
391
|
+
assert.deepStrictEqual(await reach(null), nowhere, 'anon, rejected + public');
|
|
392
|
+
assert.deepStrictEqual(await reach(author_h), everywhere, 'author, rejected');
|
|
393
|
+
});
|
|
394
|
+
test_if(capabilities.cell_gated_create && capabilities.fact_serving, "a pending post's fact is 404 to anon, even flipped public; live once approved", async () => {
|
|
395
|
+
const { fixture, author, post, hash } = await setup_pending_post_on(await setup_test(), 'pending-post-fact');
|
|
396
|
+
assert.ok(hash !== null);
|
|
397
|
+
await set_visibility(fixture, author.create_session_headers(), post.cell.id, 'public');
|
|
398
|
+
const path = `/api/cells/${post.cell.id}/facts/${hash}`;
|
|
399
|
+
assert.strictEqual(await fact_status(fixture, path, null), 404, 'anon');
|
|
400
|
+
assert.strictEqual(await fact_status(fixture, path, author.create_session_headers()), 200, 'author');
|
|
401
|
+
await moderate(fixture, post.cell.id, 'approved');
|
|
402
|
+
assert.strictEqual(await fact_status(fixture, path, null), 200, 'approved is live');
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
describe('the steward arm: a root owner moderates (parity)', () => {
|
|
406
|
+
test_if(capabilities.cell_gated_create, 'a non-admin root owner reads the pending queue and the post, and moderates it', async () => {
|
|
407
|
+
const { fixture, root, root_owner, author, post } = await setup_stewarded_post();
|
|
408
|
+
const steward_h = root_owner.create_session_headers();
|
|
409
|
+
const stranger = await fixture.create_account({ username: 'steward_stranger' });
|
|
410
|
+
const stranger_h = stranger.create_session_headers();
|
|
411
|
+
const queue = { root_id: root.cell.id, moderation: 'pending' };
|
|
412
|
+
assert.deepStrictEqual(await listed_ids(fixture, queue, steward_h), [post.cell.id]);
|
|
413
|
+
assert.deepStrictEqual(await listed_ids(fixture, queue, stranger_h), []);
|
|
414
|
+
const got = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_get', { id: post.cell.id }, steward_h), CellGetOutput);
|
|
415
|
+
assert.strictEqual(got.cell.id, post.cell.id);
|
|
416
|
+
assert.isFalse(got.can_edit, 'a steward does not edit');
|
|
417
|
+
assert.isFalse(got.can_grant, 'a steward does not manage');
|
|
418
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', { id: post.cell.id }, stranger_h), 'cell_not_found');
|
|
419
|
+
// a self-flip to public still reaches no one past author, steward, and admin
|
|
420
|
+
await set_visibility(fixture, author.create_session_headers(), post.cell.id, 'public');
|
|
421
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', { id: post.cell.id }, null), 'cell_not_found');
|
|
422
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', { id: post.cell.id }, steward_h), 'ok');
|
|
423
|
+
const by_author = await cross_rpc_call(fixture.transport, rpc_path, 'cell_moderate', { cell_id: post.cell.id, moderation: 'approved' }, author.create_session_headers());
|
|
424
|
+
assert.strictEqual(error_reason(by_author), 'cell_moderate_forbidden');
|
|
425
|
+
assert.strictEqual(await outcome(fixture, 'cell_moderate', { cell_id: post.cell.id, moderation: 'approved' }, stranger_h), 'cell_not_found');
|
|
426
|
+
const approved = await moderate(fixture, post.cell.id, 'approved', steward_h);
|
|
427
|
+
assert.strictEqual(approved.moderation, 'approved', 'the root owner moderates');
|
|
428
|
+
assert.strictEqual(approved.visibility, 'public');
|
|
429
|
+
});
|
|
430
|
+
test_if(capabilities.cell_gated_create, 'the steward arm stops at the moderator surfaces: clone and create-under are 404', async () => {
|
|
431
|
+
const { fixture, root_owner, post } = await setup_stewarded_post();
|
|
432
|
+
const steward_h = root_owner.create_session_headers();
|
|
433
|
+
assert.strictEqual(await outcome(fixture, 'cell_clone', { source_id: post.cell.id }, steward_h), 'cell_not_found');
|
|
434
|
+
assert.strictEqual(await outcome(fixture, 'cell_create', { kind: 'comment', data: {}, parent_id: post.cell.id }, steward_h), 'cell_not_found');
|
|
435
|
+
});
|
|
436
|
+
test_if(capabilities.cell_gated_create, 'the steward arm keys on the governing root, not the parent: a rejected comment under an approved post', async () => {
|
|
437
|
+
const { fixture, root_owner, author, post } = await setup_stewarded_post();
|
|
438
|
+
const steward_h = root_owner.create_session_headers();
|
|
439
|
+
await moderate(fixture, post.cell.id, 'approved', steward_h);
|
|
440
|
+
const commenter = await fixture.create_account({
|
|
441
|
+
username: 'steward_commenter',
|
|
442
|
+
roles: [PARTICIPATION_ROLE]
|
|
443
|
+
});
|
|
444
|
+
const commenter_h = commenter.create_session_headers();
|
|
445
|
+
const comment = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'comment', data: {}, parent_id: post.cell.id, visibility: 'public' }, commenter_h), CellCreateOutput);
|
|
446
|
+
assert.strictEqual(comment.cell.root_id, post.cell.root_id, 'governed by the root');
|
|
447
|
+
const rejected = await moderate(fixture, comment.cell.id, 'rejected', steward_h);
|
|
448
|
+
assert.strictEqual(rejected.moderation, 'rejected');
|
|
449
|
+
const get = { id: comment.cell.id };
|
|
450
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, steward_h), 'ok', 'root owner');
|
|
451
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, author.create_session_headers()), 'cell_not_found', "the parent's author is not the steward");
|
|
452
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, commenter_h), 'ok', 'author');
|
|
453
|
+
assert.strictEqual(await outcome(fixture, 'cell_get', get, null), 'cell_not_found', 'anon');
|
|
454
|
+
});
|
|
455
|
+
test_if(capabilities.cell_gated_create && capabilities.fact_serving, "the root owner fetches a pending post's fact; anon 404", async () => {
|
|
456
|
+
const { fixture, root_owner, post, hash } = await setup_stewarded_post_on(await setup_test(), 'stewarded-post-fact');
|
|
457
|
+
assert.ok(hash !== null);
|
|
458
|
+
const path = `/api/cells/${post.cell.id}/facts/${hash}`;
|
|
459
|
+
assert.strictEqual(await fact_status(fixture, path, null), 404, 'anon');
|
|
460
|
+
assert.strictEqual(await fact_status(fixture, path, root_owner.create_session_headers()), 200, 'steward');
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
describe('cell_grant_create refuses an unapproved contribution (parity)', () => {
|
|
464
|
+
test_if(capabilities.cell_gated_create, 'a grant on a pending post → 403 cell_grant_unapproved; non-manager 404; lands after approval', async () => {
|
|
465
|
+
const { fixture, author, post } = await setup_pending_post();
|
|
466
|
+
const friend = await fixture.create_account({ username: 'unapproved_friend' });
|
|
467
|
+
const stranger = await fixture.create_account({ username: 'unapproved_stranger' });
|
|
468
|
+
const actor_shaped = {
|
|
469
|
+
cell_id: post.cell.id,
|
|
470
|
+
level: 'viewer',
|
|
471
|
+
principal: { kind: 'actor', actor_id: friend.actor.id }
|
|
472
|
+
};
|
|
473
|
+
const role_shaped = {
|
|
474
|
+
cell_id: post.cell.id,
|
|
475
|
+
level: 'viewer',
|
|
476
|
+
principal: { kind: 'role', role: PARTICIPATION_ROLE }
|
|
477
|
+
};
|
|
478
|
+
for (const headers of [author.create_session_headers(), fixture.create_session_headers()]) {
|
|
479
|
+
for (const params of [actor_shaped, role_shaped]) {
|
|
480
|
+
const refused = await cross_rpc_call(fixture.transport, rpc_path, 'cell_grant_create', params, headers);
|
|
481
|
+
assert.ok(!refused.ok, 'a grant on a pending post must be refused');
|
|
482
|
+
assert.strictEqual(refused.error?.code, JSONRPC_ERROR_CODES.forbidden, 'a 403');
|
|
483
|
+
assert.strictEqual(error_reason(refused), 'cell_grant_unapproved');
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
assert.strictEqual(await outcome(fixture, 'cell_grant_create', actor_shaped, stranger.create_session_headers()), 'cell_not_found', 'a non-manager keeps the 404 mask');
|
|
487
|
+
await moderate(fixture, post.cell.id, 'approved');
|
|
488
|
+
for (const params of [actor_shaped, role_shaped]) {
|
|
489
|
+
expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_grant_create', params, author.create_session_headers()), CellGrantCreateOutput);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
describe('cell_clone runs the creation authorizer (parity)', () => {
|
|
494
|
+
test_if(capabilities.cell_gated_create, 'a non-admin clone of a viewable `space` root → 403, admin clone succeeds', async () => {
|
|
495
|
+
const fixture = await setup_test();
|
|
496
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: TEST_SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
|
|
497
|
+
// Every cell a clone writes is a parentless create of the source's
|
|
498
|
+
// kind, gated like `cell_create` — so a caller denied a `space` root
|
|
499
|
+
// can't clone one they can see into one they own.
|
|
500
|
+
const stranger = await fixture.create_account({ username: 'clone_stranger' });
|
|
501
|
+
const denied = await cross_rpc_call(fixture.transport, rpc_path, 'cell_clone', { source_id: space.cell.id }, stranger.create_session_headers());
|
|
502
|
+
assert.ok(!denied.ok, 'a non-admin must not clone a space root');
|
|
503
|
+
assert.strictEqual(denied.error?.code, JSONRPC_ERROR_CODES.forbidden, 'a 403');
|
|
504
|
+
assert.strictEqual(error_reason(denied), 'cell_create_forbidden');
|
|
505
|
+
const cloned = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_clone', { source_id: space.cell.id }, fixture.create_session_headers()), CellCloneOutput);
|
|
506
|
+
assert.strictEqual(cloned.cell.kind, SPACE_CELL_KIND, 'the admin clones the space');
|
|
507
|
+
assert.notStrictEqual(cloned.cell.id, space.cell.id);
|
|
508
|
+
assert.strictEqual(cloned.cell.moderation, null, 'a clone stamps no moderation');
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_ref_admission.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_ref_admission.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"cell_ref_admission.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_ref_admission.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsD9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AA6B1E,eAAO,MAAM,uCAAuC,GACnD,SAAS,uCAAuC,KAC9C,IA8eF,CAAC"}
|
|
@@ -40,7 +40,7 @@ import { CellCloneOutput, CellCreateOutput, CellGetOutput, CellUpdateOutput } fr
|
|
|
40
40
|
import { CellGrantCreateOutput } from '../../auth/cell_grant_action_specs.js';
|
|
41
41
|
import { create_rpc_post_init } from '../rpc_helpers.js';
|
|
42
42
|
import { test_if } from './capabilities.js';
|
|
43
|
-
import { cross_rpc_call, error_reason, expect_output } from './
|
|
43
|
+
import { cross_rpc_call, error_reason, expect_output } from './cross_rpc.js';
|
|
44
44
|
import { cross_put_fact } from './fact_serving.js';
|
|
45
45
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
46
46
|
/** The admission refusal's reason, on both spines. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_relations.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_relations.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"cell_relations.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_relations.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA+G9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAuE1E,eAAO,MAAM,mCAAmC,GAC/C,SAAS,uCAAuC,KAC9C,IAirCF,CAAC"}
|