@fuzdev/fuz_app 0.148.0 → 0.150.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 +11 -9
- package/dist/actions/action_bridge.d.ts +7 -0
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_bridge.js +10 -0
- package/dist/actions/action_event.d.ts.map +1 -1
- package/dist/actions/action_event.js +14 -1
- package/dist/actions/action_event_types.d.ts +1 -1
- package/dist/actions/action_event_types.d.ts.map +1 -1
- package/dist/actions/perform_action.d.ts +17 -1
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +30 -7
- package/dist/actions/register_action_ws.d.ts +8 -2
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +24 -4
- package/dist/actions/transports_ws_auth_guard.d.ts +6 -4
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +9 -6
- package/dist/actions/ws_endpoint_spec.d.ts +1 -1
- package/dist/auth/CLAUDE.md +72 -33
- package/dist/auth/account_action_specs.d.ts +12 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +16 -2
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +7 -4
- package/dist/auth/actor_lookup_queries.d.ts +3 -2
- package/dist/auth/actor_lookup_queries.d.ts.map +1 -1
- package/dist/auth/actor_lookup_queries.js +6 -3
- package/dist/auth/actor_search_queries.d.ts +5 -2
- package/dist/auth/actor_search_queries.d.ts.map +1 -1
- package/dist/auth/actor_search_queries.js +11 -4
- package/dist/auth/admin_action_specs.d.ts +4 -4
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +33 -12
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +35 -6
- package/dist/auth/all_cell_actions.d.ts +11 -2
- package/dist/auth/all_cell_actions.d.ts.map +1 -1
- package/dist/auth/all_cell_actions.js +2 -1
- package/dist/auth/audit_emitter.d.ts +23 -3
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +14 -4
- package/dist/auth/audit_log_schema.d.ts +1 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +9 -5
- package/dist/auth/bootstrap_account.d.ts +25 -1
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +37 -4
- package/dist/auth/bootstrap_routes.d.ts +3 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +5 -4
- package/dist/auth/cell_action_specs.d.ts +215 -15
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +103 -16
- package/dist/auth/cell_actions.d.ts +40 -18
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +126 -19
- package/dist/auth/cell_audit_events.d.ts +1 -1
- package/dist/auth/cell_audit_events.d.ts.map +1 -1
- package/dist/auth/cell_audit_events.js +3 -2
- package/dist/auth/cell_audit_metadata.d.ts +11 -0
- package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
- package/dist/auth/cell_audit_metadata.js +10 -0
- package/dist/auth/cell_grant_action_specs.d.ts +7 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_grant_action_specs.js +7 -0
- package/dist/auth/cell_grant_actions.d.ts +36 -4
- package/dist/auth/cell_grant_actions.d.ts.map +1 -1
- package/dist/auth/cell_grant_actions.js +20 -3
- package/dist/auth/credential_type_schema.d.ts +3 -2
- package/dist/auth/credential_type_schema.d.ts.map +1 -1
- package/dist/auth/credential_type_schema.js +3 -2
- package/dist/auth/role_grant_offer_action_specs.d.ts +18 -2
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +30 -6
- package/dist/auth/role_grant_offer_actions.d.ts +9 -4
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +58 -7
- package/dist/auth/role_grant_offer_notifications.d.ts +3 -2
- package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_notifications.js +3 -2
- package/dist/auth/role_grant_offer_queries.d.ts +20 -0
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +35 -0
- package/dist/auth/role_grant_queries.d.ts +27 -9
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +40 -13
- package/dist/auth/role_schema.d.ts +14 -8
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +6 -3
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +20 -0
- package/dist/db/CLAUDE.md +9 -2
- package/dist/db/cell_ddl.d.ts +9 -1
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +11 -1
- package/dist/db/cell_queries.d.ts +53 -7
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +61 -7
- package/dist/http/CLAUDE.md +14 -2
- package/dist/http/body_limit.d.ts +3 -1
- package/dist/http/body_limit.d.ts.map +1 -1
- package/dist/http/body_limit.js +6 -1
- package/dist/http/error_schemas.d.ts +8 -0
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -0
- package/dist/http/jsonrpc.d.ts +9 -0
- package/dist/http/jsonrpc.d.ts.map +1 -1
- package/dist/http/jsonrpc.js +17 -0
- package/dist/http/jsonrpc_errors.d.ts +40 -15
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +99 -35
- package/dist/http/proxy.d.ts +4 -2
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +7 -2
- package/dist/http/surface.d.ts +3 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +7 -4
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +3 -2
- package/dist/schema_meta.d.ts +92 -1
- package/dist/schema_meta.d.ts.map +1 -1
- package/dist/schema_meta.js +117 -0
- package/dist/server/app_server.d.ts +13 -5
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +32 -8
- package/dist/testing/CLAUDE.md +196 -82
- package/dist/testing/adversarial_input_cases.d.ts +67 -7
- package/dist/testing/adversarial_input_cases.d.ts.map +1 -1
- package/dist/testing/adversarial_input_cases.js +210 -165
- package/dist/testing/app_server.d.ts +3 -1
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.d.ts +3 -2
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +5 -3
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/actor_lookup.js +27 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts +6 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/audit_cross_helpers.js +11 -0
- package/dist/testing/cross_backend/capabilities.d.ts +29 -5
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +10 -6
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +117 -2
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +109 -13
- package/dist/testing/cross_backend/cell_moderate.d.ts +7 -1
- package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_moderate.js +68 -4
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_ref_admission.js +18 -9
- package/dist/testing/cross_backend/conformance_table.d.ts +5 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +12 -10
- package/dist/testing/cross_backend/cross_rpc.d.ts +3 -1
- package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -1
- package/dist/testing/cross_backend/cross_rpc.js +2 -2
- package/dist/testing/cross_backend/default_backend_configs.d.ts +9 -7
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +3 -3
- package/dist/testing/cross_backend/differential.d.ts +7 -0
- package/dist/testing/cross_backend/differential.d.ts.map +1 -1
- package/dist/testing/cross_backend/differential.js +17 -0
- package/dist/testing/cross_backend/expected_schema.json +1 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +5 -4
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +74 -2
- package/dist/testing/cross_backend/rpc_adversarial.d.ts +9 -0
- package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -1
- package/dist/testing/cross_backend/rpc_adversarial.js +16 -6
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
- package/dist/testing/cross_backend/setup.d.ts +2 -2
- package/dist/testing/cross_backend/spine_suites.d.ts +85 -5
- package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_suites.js +291 -20
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +15 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +24 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +18 -19
- 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 +66 -42
- package/dist/testing/cross_backend/testing_backdoor.d.ts +27 -4
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +136 -41
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +30 -15
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +52 -35
- package/dist/testing/cross_backend/transport_composition.js +3 -3
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +6 -3
- package/dist/testing/rpc_attack_surface.d.ts +10 -0
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +15 -3
- package/dist/testing/rpc_envelope_cases.d.ts +5 -2
- package/dist/testing/rpc_envelope_cases.d.ts.map +1 -1
- package/dist/testing/rpc_envelope_cases.js +4 -1
- package/dist/testing/rpc_round_trip.js +3 -3
- package/dist/testing/schema_generators.d.ts +99 -2
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +244 -76
- package/dist/testing/stubs.d.ts +4 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/zod.d.ts +9 -0
- package/dist/zod.d.ts.map +1 -1
- package/dist/zod.js +20 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/audit_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsB9B,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AAErF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAIvE,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,eAAe,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,KAAK,cAAc,CAAC;IAC5F,2FAA2F;IAC3F,QAAQ,CAAC,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5F,QAAQ,CAAC,2BAA2B,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjG,8FAA8F;IAC9F,QAAQ,CAAC,kCAAkC,CAAC,EAAE,OAAO,CAAC;CACtD;AAED
|
|
1
|
+
{"version":3,"file":"audit_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/audit_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsB9B,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AAErF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAIvE,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,eAAe,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,KAAK,cAAc,CAAC;IAC5F,2FAA2F;IAC3F,QAAQ,CAAC,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5F,QAAQ,CAAC,2BAA2B,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjG,8FAA8F;IAC9F,QAAQ,CAAC,kCAAkC,CAAC,EAAE,OAAO,CAAC;CACtD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC/B,SAAS,gBAAgB,EACzB,WAAU,MAAuB,KAC/B,OAAO,CAAC,IAAI,CAmBd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,IAAI,CAAC,gBAAgB,EAAE,oCAAoC,CAAC,EACrE,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACxC,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,SAAQ,aAAa,CAAC,MAAM,CAAM,KAChC,IASF,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,GAC7B,SAAS,gBAAgB,EACzB,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAU,MAAuB,KAC/B,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAe/C,CAAC"}
|
|
@@ -26,12 +26,23 @@ import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
|
26
26
|
* backend must mount the action — every spine test binary does; in-process
|
|
27
27
|
* suites mount `create_testing_drain_effects_action`.
|
|
28
28
|
*
|
|
29
|
+
* Refuses an answer without `tracked: true`. A binary whose audit emitter was
|
|
30
|
+
* built without in-flight tracking answers the drain at once, waiting on
|
|
31
|
+
* nothing, and its audit reads race the writes — an absent row then reads as
|
|
32
|
+
* a pass. Every audit read runs through here, so each one checks the barrier
|
|
33
|
+
* is live.
|
|
34
|
+
*
|
|
29
35
|
* @param fixture - the per-test fixture, whose daemon token authorizes the drain
|
|
30
36
|
* @param rpc_path - the RPC endpoint the drain action is mounted on
|
|
31
37
|
*/
|
|
32
38
|
export const cross_drain_effects = async (fixture, rpc_path = SPINE_RPC_PATH) => {
|
|
33
39
|
const drained = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
|
|
34
40
|
assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
|
|
41
|
+
const tracked = typeof drained.result === 'object' && drained.result !== null
|
|
42
|
+
? drained.result.tracked
|
|
43
|
+
: undefined;
|
|
44
|
+
assert.strictEqual(tracked, true, "_testing_drain_effects must answer tracked: true — the backend's audit emitter does not " +
|
|
45
|
+
`track in-flight writes, so the drain waits on nothing (got ${JSON.stringify(drained.result)})`);
|
|
35
46
|
};
|
|
36
47
|
/**
|
|
37
48
|
* Assert an audit row's `metadata` carries `expected` — each field compared
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
|
+
import type { TestingBackdoorMethod } from './spine_surface_constants.js';
|
|
2
3
|
/**
|
|
3
4
|
* Optional behaviors a backend may support. Each flag's TSDoc names the
|
|
4
5
|
* tests that gate on it; add a new flag here before referencing it from
|
|
@@ -61,10 +62,12 @@ export interface BackendCapabilities {
|
|
|
61
62
|
/**
|
|
62
63
|
* The cell-gated fact-serving route (`GET /api/cells/:cell_id/facts/:hash`)
|
|
63
64
|
* is live-mounted on the backend, its DB carries the `fuz_facts` migration
|
|
64
|
-
* namespace, and it
|
|
65
|
+
* namespace, and it mounts the `_testing_put_fact` seeder — declared in
|
|
66
|
+
* `testing_backdoors`, which `describe_spine_cross_suites` requires. Gates
|
|
65
67
|
* `describe_fact_serving_cross_tests`' per-reference read model (cell-scoped
|
|
66
68
|
* admit via a viewable cell, cross-owner-dedup-no-leak, 404-mask),
|
|
67
|
-
* `describe_cell_ref_admission_cross_tests
|
|
69
|
+
* `describe_cell_ref_admission_cross_tests` (its grant, item, and clone
|
|
70
|
+
* cases with `cell_relations`), and the fact cases of the
|
|
68
71
|
* moderation suite. Like cells, the serve route stays off the standard
|
|
69
72
|
* declared surface. The admin bare-hash route is its own flag,
|
|
70
73
|
* `fact_serving_admin_hash`.
|
|
@@ -191,6 +194,25 @@ export interface BackendCapabilities {
|
|
|
191
194
|
* Deno, Bun, and the Rust spine, which send the response as built.
|
|
192
195
|
*/
|
|
193
196
|
readonly ws_refusal_body: boolean;
|
|
197
|
+
/**
|
|
198
|
+
* The `_testing_*` backdoors the backend mounts on its RPC endpoint, from
|
|
199
|
+
* `TESTING_BACKDOOR_METHODS` (a consumer's own seams stay off it). Not a
|
|
200
|
+
* flag, but declared like a `MountFlag`: no family default, so every config
|
|
201
|
+
* states it. `describe_testing_backdoor_cross_tests` checks the declaration
|
|
202
|
+
* against the backend — a declared method must be mounted, an undeclared one
|
|
203
|
+
* must answer `method_not_found` — and runs its credential-gate rows over
|
|
204
|
+
* the declared ones; `describe_spine_cross_suites` requires `_testing_reset`
|
|
205
|
+
* and `_testing_drain_effects` (the per-test reset and the audit barrier),
|
|
206
|
+
* skips a suite whose backdoor is undeclared, naming it — `credential_deadline`
|
|
207
|
+
* (`_testing_shorten_credential_expiry`), `origin_conformance`
|
|
208
|
+
* (`_testing_origin_check`), `ws_outbound_overflow`
|
|
209
|
+
* (`_testing_flood_notifications`) — and requires `_testing_put_fact` of a
|
|
210
|
+
* backend declaring `fact_serving`.
|
|
211
|
+
*
|
|
212
|
+
* fuz_app's spine presets declare every one; the in-process transport none
|
|
213
|
+
* (the backdoor suites are cross-process only).
|
|
214
|
+
*/
|
|
215
|
+
readonly testing_backdoors: ReadonlyArray<TestingBackdoorMethod>;
|
|
194
216
|
}
|
|
195
217
|
/**
|
|
196
218
|
* The capabilities that describe a backend's own mounts, never a runtime
|
|
@@ -203,8 +225,9 @@ export interface BackendCapabilities {
|
|
|
203
225
|
*/
|
|
204
226
|
export type MountFlag = keyof Pick<BackendCapabilities, 'ws' | 'sse' | 'cell_crud' | 'cell_relations' | 'cell_gated_create' | 'fact_serving' | 'fact_serving_admin_hash' | 'ready' | 'peer_request' | 'ws_account_actions' | 'ws_role_gated'>;
|
|
205
227
|
/**
|
|
206
|
-
* A family's default capabilities: every flag but the `MountFlag`s
|
|
207
|
-
* each backend config states itself. Spread
|
|
228
|
+
* A family's default capabilities: every flag but the `MountFlag`s and the
|
|
229
|
+
* `testing_backdoors` list, which each backend config states itself. Spread
|
|
230
|
+
* one and add every mount flag and the backdoor list:
|
|
208
231
|
*
|
|
209
232
|
* ```ts
|
|
210
233
|
* capabilities: {
|
|
@@ -213,10 +236,11 @@ export type MountFlag = keyof Pick<BackendCapabilities, 'ws' | 'sse' | 'cell_cru
|
|
|
213
236
|
* cell_crud: false, cell_relations: false, cell_gated_create: false,
|
|
214
237
|
* fact_serving: false, fact_serving_admin_hash: false,
|
|
215
238
|
* ws_account_actions: false, ws_role_gated: false,
|
|
239
|
+
* testing_backdoors: ['_testing_reset', '_testing_drain_effects'],
|
|
216
240
|
* }
|
|
217
241
|
* ```
|
|
218
242
|
*/
|
|
219
|
-
export type FamilyDefaultCapabilities = Omit<BackendCapabilities, MountFlag>;
|
|
243
|
+
export type FamilyDefaultCapabilities = Omit<BackendCapabilities, MountFlag | 'testing_backdoors'>;
|
|
220
244
|
/**
|
|
221
245
|
* Capability declarations for the in-process Hono transport. Nearly every
|
|
222
246
|
* flag is `true` because in-process testing exercises the full backend
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkD9B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,iBAAiB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;CACjE;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,IAAI,CACjC,mBAAmB,EACjB,IAAI,GACJ,KAAK,GACL,WAAW,GACX,gBAAgB,GAChB,mBAAmB,GACnB,cAAc,GACd,yBAAyB,GACzB,OAAO,GACP,cAAc,GACd,oBAAoB,GACpB,eAAe,CACjB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,mBAAmB,CAAC,CAAC;AAEnG;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBA4BpC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
|
|
@@ -28,9 +28,9 @@ import '../assert_dev_env.js';
|
|
|
28
28
|
* `rust_spine_stub_backend_config.ts` (the rust family default plus every mount flag).
|
|
29
29
|
*
|
|
30
30
|
* **Mount flags have no default.** A flag naming something a backend mounts
|
|
31
|
-
* (a `MountFlag`) is never inherited
|
|
32
|
-
* `FamilyDefaultCapabilities`, which omits them, so every
|
|
33
|
-
* each one. The family defaults carry only what the family guarantees — the
|
|
31
|
+
* (a `MountFlag`) is never inherited, nor is the `testing_backdoors` list: the
|
|
32
|
+
* family defaults are `FamilyDefaultCapabilities`, which omits them, so every
|
|
33
|
+
* backend config states each one. The family defaults carry only what the family guarantees — the
|
|
34
34
|
* declared spine surface's mounts (`account_lifecycle`, `account_status`) and
|
|
35
35
|
* the runtime behavior of its reference runtime (`ws_handshake_pipelining`,
|
|
36
36
|
* `ws_refusal_body`: Node for TS, hyper for Rust), which a backend on another
|
|
@@ -38,8 +38,9 @@ import '../assert_dev_env.js';
|
|
|
38
38
|
*
|
|
39
39
|
* **Gating flags only.** `BackendCapabilities` holds only flags a suite
|
|
40
40
|
* actually gates on (each has a `test_if(capabilities.X, ...)` reader, or —
|
|
41
|
-
* `ws_refusal_body` — picks how a case reads a refusal)
|
|
42
|
-
*
|
|
41
|
+
* `ws_refusal_body` — picks how a case reads a refusal), plus the
|
|
42
|
+
* `testing_backdoors` list the backdoor suite and `describe_spine_cross_suites`
|
|
43
|
+
* read. A wiring fact no suite reads is not a capability.
|
|
43
44
|
*
|
|
44
45
|
* @module
|
|
45
46
|
*/
|
|
@@ -81,7 +82,10 @@ export const in_process_capabilities = Object.freeze({
|
|
|
81
82
|
ws_role_gated: false,
|
|
82
83
|
// Read only by the front-door table's WS rows, which skip in-process (no
|
|
83
84
|
// socket to upgrade); Hono's own refusals carry their bodies.
|
|
84
|
-
ws_refusal_body: true
|
|
85
|
+
ws_refusal_body: true,
|
|
86
|
+
// The in-process app mounts no `_testing_*` action; the suites reading this
|
|
87
|
+
// are cross-process only.
|
|
88
|
+
testing_backdoors: Object.freeze([])
|
|
85
89
|
});
|
|
86
90
|
/**
|
|
87
91
|
* Conditional `test()` wrapper — registers a vitest case only when
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0E9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAgE1E,eAAO,MAAM,8BAA8B,GAC1C,SAAS,uCAAuC,KAC9C,IA4uBF,CAAC"}
|
|
@@ -38,7 +38,13 @@ import '../assert_dev_env.js';
|
|
|
38
38
|
* accepted as a list filter that matches nothing;
|
|
39
39
|
* - a `kind` of exactly `CELL_KIND_LENGTH_MAX` UTF-16 code units is stored
|
|
40
40
|
* whole, and one unit past it is `invalid_params` on create and as a list
|
|
41
|
-
* filter
|
|
41
|
+
* filter;
|
|
42
|
+
* - a `data` that is no `CellData` — not an object, or a `label` / `summary`
|
|
43
|
+
* that is no string — is 400 `invalid_params` with the generic message and
|
|
44
|
+
* no reason on `cell_create`, `cell_update` and (with
|
|
45
|
+
* `capabilities.cell_relations`) `cell_clone`'s `with_data_patch`, ahead of
|
|
46
|
+
* every cell check (a missing or hidden cell refuses it the same way, never
|
|
47
|
+
* with the 404 mask), and writes no cell.
|
|
42
48
|
*
|
|
43
49
|
* The visibility-manage-tier 403 (`cell_visibility_manage_only`) needs a
|
|
44
50
|
* non-owner editor, which only a `cell_grant` can produce, so it lives in
|
|
@@ -48,11 +54,64 @@ import '../assert_dev_env.js';
|
|
|
48
54
|
* @module
|
|
49
55
|
*/
|
|
50
56
|
import { describe, assert } from 'vitest';
|
|
51
|
-
import {
|
|
57
|
+
import { jsonrpc_error_messages } from '../../http/jsonrpc_errors.js';
|
|
58
|
+
import { CELL_KIND_LENGTH_MAX, CELL_LIST_LIMIT_MAX, CellCreateOutput, CellDeleteOutput, CellGetOutput, CellListOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
|
|
52
59
|
import { test_if } from './capabilities.js';
|
|
53
60
|
import { cross_rpc_call, error_reason, expect_output } from './cross_rpc.js';
|
|
54
61
|
import { assert_iso8601_seconds, assert_iso8601_seconds_nullable } from './wire_shapes.js';
|
|
55
62
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
63
|
+
/**
|
|
64
|
+
* `data` values that are no `CellData` — not an object, or an object whose
|
|
65
|
+
* `label` / `summary` is no string. Twin of the Rust spine's mistyped-data
|
|
66
|
+
* table in its cell authz tests.
|
|
67
|
+
*/
|
|
68
|
+
const MISTYPED_CELL_DATA = [
|
|
69
|
+
'not_an_object',
|
|
70
|
+
42,
|
|
71
|
+
true,
|
|
72
|
+
[1],
|
|
73
|
+
{ label: 123 },
|
|
74
|
+
{ label: null },
|
|
75
|
+
{ summary: {} },
|
|
76
|
+
{ summary: false }
|
|
77
|
+
];
|
|
78
|
+
/** The message every schema refusal answers, naming no field. */
|
|
79
|
+
const INVALID_PARAMS_MESSAGE = jsonrpc_error_messages.invalid_params().message;
|
|
80
|
+
/**
|
|
81
|
+
* Assert `r` is a schema refusal: 400 `invalid_params` with the generic
|
|
82
|
+
* message and no `data`, save the Zod `issues` a TS spine adds under DEV
|
|
83
|
+
* alone — an array of issue objects, each with a string `code` and
|
|
84
|
+
* `message`, and never a `reason`.
|
|
85
|
+
*/
|
|
86
|
+
const assert_schema_refusal = (r, label) => {
|
|
87
|
+
const detail = `${label}: got ${r.status} ${JSON.stringify(r.error ?? r.result)}`;
|
|
88
|
+
assert.strictEqual(r.status, 400, detail);
|
|
89
|
+
const { error } = r;
|
|
90
|
+
assert.ok(error, detail);
|
|
91
|
+
assert.strictEqual(error.code, -32602, detail);
|
|
92
|
+
assert.strictEqual(error.message, INVALID_PARAMS_MESSAGE, detail);
|
|
93
|
+
const { data } = error;
|
|
94
|
+
if (data === undefined)
|
|
95
|
+
return;
|
|
96
|
+
assert.ok(typeof data === 'object' && data !== null && Object.keys(data).join() === 'issues', `${detail} — data beyond the DEV-only issues`);
|
|
97
|
+
const { issues } = data;
|
|
98
|
+
assert.ok(Array.isArray(issues), `${detail} — DEV-only issues that are no array`);
|
|
99
|
+
for (const issue of issues) {
|
|
100
|
+
assert.ok(typeof issue === 'object' &&
|
|
101
|
+
issue !== null &&
|
|
102
|
+
typeof issue.code === 'string' &&
|
|
103
|
+
typeof issue.message === 'string', `${detail} — a DEV-only issue that is no Zod issue`);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Assert `r` is the IDOR mask: 404 with reason `cell_not_found` — the
|
|
108
|
+
* well-typed control beside a schema refusal of the same call.
|
|
109
|
+
*/
|
|
110
|
+
const assert_cell_not_found = (r, label) => {
|
|
111
|
+
const detail = `${label}: got ${r.status} ${JSON.stringify(r.error ?? r.result)}`;
|
|
112
|
+
assert.strictEqual(r.status, 404, detail);
|
|
113
|
+
assert.strictEqual(error_reason(r), 'cell_not_found', detail);
|
|
114
|
+
};
|
|
56
115
|
export const describe_cell_crud_cross_tests = (options) => {
|
|
57
116
|
const { setup_test, capabilities } = options;
|
|
58
117
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
@@ -327,5 +386,61 @@ export const describe_cell_crud_cross_tests = (options) => {
|
|
|
327
386
|
}
|
|
328
387
|
}
|
|
329
388
|
});
|
|
389
|
+
describe('mistyped data', () => {
|
|
390
|
+
/** Count every cell, read as the keeper (an admin sees every row). */
|
|
391
|
+
const count_cells = async (t, keeper_headers) => {
|
|
392
|
+
const { cells } = expect_output(await cross_rpc_call(t, rpc_path, 'cell_list', { limit: CELL_LIST_LIMIT_MAX }, keeper_headers), CellListOutput);
|
|
393
|
+
assert.ok(cells.length < CELL_LIST_LIMIT_MAX, 'the count needs a page that holds every cell');
|
|
394
|
+
return cells.length;
|
|
395
|
+
};
|
|
396
|
+
test_if(capabilities.cell_crud, 'cell_create and cell_update refuse it as invalid_params before any cell check', async () => {
|
|
397
|
+
const fixture = await setup_test();
|
|
398
|
+
const writer = await fixture.create_account({ username: 'cell_data_writer' });
|
|
399
|
+
const stranger = await fixture.create_account({ username: 'cell_data_stranger' });
|
|
400
|
+
const t = fixture.fresh_transport();
|
|
401
|
+
const keeper_headers = fixture.create_session_headers();
|
|
402
|
+
const writer_headers = writer.create_session_headers();
|
|
403
|
+
// the keeper's private cell, hidden from the stranger; the keeper
|
|
404
|
+
// creates it so a backend that refuses a non-admin root runs the case
|
|
405
|
+
const hidden = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, keeper_headers), CellCreateOutput).cell.id;
|
|
406
|
+
const missing = crypto.randomUUID();
|
|
407
|
+
const before = await count_cells(t, keeper_headers);
|
|
408
|
+
for (const data of MISTYPED_CELL_DATA) {
|
|
409
|
+
const shown = JSON.stringify(data);
|
|
410
|
+
assert_schema_refusal(await cross_rpc_call(t, rpc_path, 'cell_create', { data }, writer_headers), `cell_create data ${shown}`);
|
|
411
|
+
for (const [cell_id, headers, who] of [
|
|
412
|
+
[missing, writer_headers, 'a missing cell'],
|
|
413
|
+
[hidden, stranger.create_session_headers(), 'a hidden cell']
|
|
414
|
+
]) {
|
|
415
|
+
assert_schema_refusal(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id, data }, headers), `cell_update of ${who} data ${shown}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// the control: well-typed, the same call meets the 404 mask, so the
|
|
419
|
+
// 400s above came before the cell check
|
|
420
|
+
assert_cell_not_found(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id: hidden, data: {} }, stranger.create_session_headers()), 'cell_update of a hidden cell data {}');
|
|
421
|
+
assert.strictEqual(await count_cells(t, keeper_headers), before, 'a refused write wrote a cell');
|
|
422
|
+
});
|
|
423
|
+
test_if(capabilities.cell_crud && capabilities.cell_relations, 'cell_clone refuses it in with_data_patch as invalid_params before any cell check', async () => {
|
|
424
|
+
const fixture = await setup_test();
|
|
425
|
+
const writer = await fixture.create_account({ username: 'cell_patch_writer' });
|
|
426
|
+
const stranger = await fixture.create_account({ username: 'cell_patch_stranger' });
|
|
427
|
+
const t = fixture.fresh_transport();
|
|
428
|
+
const keeper_headers = fixture.create_session_headers();
|
|
429
|
+
const hidden = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind: 'note', data: {} }, keeper_headers), CellCreateOutput).cell.id;
|
|
430
|
+
const missing = crypto.randomUUID();
|
|
431
|
+
const before = await count_cells(t, keeper_headers);
|
|
432
|
+
for (const with_data_patch of MISTYPED_CELL_DATA) {
|
|
433
|
+
for (const [source_id, headers, who] of [
|
|
434
|
+
[missing, writer.create_session_headers(), 'a missing cell'],
|
|
435
|
+
[hidden, stranger.create_session_headers(), 'a hidden cell']
|
|
436
|
+
]) {
|
|
437
|
+
assert_schema_refusal(await cross_rpc_call(t, rpc_path, 'cell_clone', { source_id, with_data_patch }, headers), `cell_clone of ${who} with_data_patch ${JSON.stringify(with_data_patch)}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// the control: well-typed, the same clone meets the 404 mask
|
|
441
|
+
assert_cell_not_found(await cross_rpc_call(t, rpc_path, 'cell_clone', { source_id: hidden, with_data_patch: { label: 'patched' } }, stranger.create_session_headers()), 'cell_clone of a hidden cell with_data_patch {label}');
|
|
442
|
+
assert.strictEqual(await count_cells(t, keeper_headers), before, 'a refused clone wrote a cell');
|
|
443
|
+
});
|
|
444
|
+
});
|
|
330
445
|
});
|
|
331
446
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_gated_create.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_gated_create.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"cell_gated_create.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_gated_create.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuE9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAQ1E,eAAO,MAAM,sCAAsC,GAClD,SAAS,uCAAuC,KAC9C,IAwkBF,CAAC"}
|
|
@@ -14,9 +14,10 @@ import '../assert_dev_env.js';
|
|
|
14
14
|
* `create_test_cell_gated_create_authorize`, the Rust `testing_spine_stub` via
|
|
15
15
|
* `TestCellGatedCreateAuthorize`): admin bypasses; a non-admin creating a
|
|
16
16
|
* `kind: 'space'` **root** is denied (admin-only); a **contribution** under a
|
|
17
|
-
* space is gated by the space's `
|
|
18
|
-
*
|
|
19
|
-
* stay open. The suite asserts all
|
|
17
|
+
* space is gated by the space's `policy[kind] = {min_role, moderation_required}`
|
|
18
|
+
* (the root's manage-tier `policy` column), with the moderation outcome folded
|
|
19
|
+
* into the verdict; plain parentless creates stay open. The suite asserts all
|
|
20
|
+
* spines agree on:
|
|
20
21
|
*
|
|
21
22
|
* - **root-create admin-only** — a non-admin `space` root → **403**
|
|
22
23
|
* `cell_create_forbidden`; admin → succeeds.
|
|
@@ -37,6 +38,15 @@ import '../assert_dev_env.js';
|
|
|
37
38
|
* - **clone is no side door** — `cell_clone` runs the authorizer for every cell
|
|
38
39
|
* it writes as a parentless create, so a non-admin clone of a viewable
|
|
39
40
|
* `space` root → **403** `cell_create_forbidden`; admin → succeeds.
|
|
41
|
+
* - **the root policy is the manager's** — `cell_policy_set` writes it (the new
|
|
42
|
+
* and the previous policy audited), answers an editor-grant holder **403**
|
|
43
|
+
* `cell_policy_manage_only` and a non-viewer the 404 mask, refuses a
|
|
44
|
+
* contribution `cell_policy_root_only` (as `cell_create` refuses a policy
|
|
45
|
+
* beside a `parent_id`), and passes each policy through the authorizer's
|
|
46
|
+
* validation (`cell_policy_invalid`); an editor rewriting the root's
|
|
47
|
+
* `data.policy` leaves moderation as the column says. The refusal with no
|
|
48
|
+
* authorizer mounted (`cell_policy_unsupported`) can't run here — both spines
|
|
49
|
+
* mount one — and is pinned in-process on each.
|
|
40
50
|
*
|
|
41
51
|
* Gated on `capabilities.cell_gated_create` — `true` only on the reference
|
|
42
52
|
* spine binaries that mount the policy, so it skips for generic consumers and
|
|
@@ -46,7 +56,9 @@ import '../assert_dev_env.js';
|
|
|
46
56
|
* @module
|
|
47
57
|
*/
|
|
48
58
|
import { describe, assert } from 'vitest';
|
|
49
|
-
import { CellCreateOutput } from '../../auth/cell_action_specs.js';
|
|
59
|
+
import { CellCreateOutput, CellGetOutput, CellPolicySetOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
|
|
60
|
+
import { CellGrantCreateOutput } from '../../auth/cell_grant_action_specs.js';
|
|
61
|
+
import { list_audit_events } from './audit_cross_helpers.js';
|
|
50
62
|
import { test_if } from './capabilities.js';
|
|
51
63
|
import { cross_rpc_call, error_reason, expect_output } from './cross_rpc.js';
|
|
52
64
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
@@ -61,7 +73,7 @@ export const describe_cell_gated_create_cross_tests = (options) => {
|
|
|
61
73
|
const denied = await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {} }, stranger.create_session_headers());
|
|
62
74
|
assert.ok(!denied.ok, 'a non-admin must not create a space root');
|
|
63
75
|
assert.strictEqual(error_reason(denied), 'cell_create_forbidden');
|
|
64
|
-
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);
|
|
76
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {}, policy: TEST_SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
|
|
65
77
|
assert.strictEqual(space.cell.kind, SPACE_CELL_KIND, 'admin creates the space root');
|
|
66
78
|
assert.strictEqual(space.cell.parent_id, null, 'a root is parentless');
|
|
67
79
|
assert.strictEqual(space.cell.root_id, null, 'a root has no governing root');
|
|
@@ -69,7 +81,7 @@ export const describe_cell_gated_create_cross_tests = (options) => {
|
|
|
69
81
|
});
|
|
70
82
|
test_if(capabilities.cell_gated_create, 'a contribution under a public space: unauthorized → 403, participant → allowed', async () => {
|
|
71
83
|
const fixture = await setup_test();
|
|
72
|
-
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);
|
|
84
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {}, policy: TEST_SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
|
|
73
85
|
// A non-participant sees the public space but can't contribute → 403.
|
|
74
86
|
const stranger = await fixture.create_account({ username: 'post_stranger' });
|
|
75
87
|
const forbidden = await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id }, stranger.create_session_headers());
|
|
@@ -86,7 +98,7 @@ export const describe_cell_gated_create_cross_tests = (options) => {
|
|
|
86
98
|
});
|
|
87
99
|
test_if(capabilities.cell_gated_create, 'moderation is set per `moderation_required` (post → pending+private, react → approved)', async () => {
|
|
88
100
|
const fixture = await setup_test();
|
|
89
|
-
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);
|
|
101
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {}, policy: TEST_SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
|
|
90
102
|
const participant = await fixture.create_account({
|
|
91
103
|
username: 'mod_participant',
|
|
92
104
|
roles: [PARTICIPATION_ROLE]
|
|
@@ -103,7 +115,7 @@ export const describe_cell_gated_create_cross_tests = (options) => {
|
|
|
103
115
|
test_if(capabilities.cell_gated_create, 'a contribution under a hidden (private) space is 404-masked (not 403)', async () => {
|
|
104
116
|
const fixture = await setup_test();
|
|
105
117
|
// A private space — only admin/owner can view it.
|
|
106
|
-
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: TEST_SPACE_POLICY, visibility: 'private' }, fixture.create_session_headers()), CellCreateOutput);
|
|
118
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {}, policy: TEST_SPACE_POLICY, visibility: 'private' }, fixture.create_session_headers()), CellCreateOutput);
|
|
107
119
|
const stranger = await fixture.create_account({ username: 'hidden_stranger' });
|
|
108
120
|
const masked = await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id }, stranger.create_session_headers());
|
|
109
121
|
assert.ok(!masked.ok, 'a stranger must not contribute under a hidden space');
|
|
@@ -111,14 +123,14 @@ export const describe_cell_gated_create_cross_tests = (options) => {
|
|
|
111
123
|
});
|
|
112
124
|
test_if(capabilities.cell_gated_create, 'a deep contribution (comment under a post) resolves policy from the governing root, not the parent', async () => {
|
|
113
125
|
const fixture = await setup_test();
|
|
114
|
-
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);
|
|
126
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {}, policy: TEST_SPACE_POLICY, visibility: 'public' }, fixture.create_session_headers()), CellCreateOutput);
|
|
115
127
|
const participant = await fixture.create_account({
|
|
116
128
|
username: 'deep_participant',
|
|
117
129
|
roles: [PARTICIPATION_ROLE]
|
|
118
130
|
});
|
|
119
131
|
// A post lives directly under the space — the shallow case
|
|
120
|
-
// (`root_id === parent_id`), so the handler reuses the parent row
|
|
121
|
-
// `
|
|
132
|
+
// (`root_id === parent_id`), so the handler reuses the parent row's
|
|
133
|
+
// `policy`. The post is gated `moderation_required: true`, so it's
|
|
122
134
|
// born pending + private; the author can still view their own pending
|
|
123
135
|
// cell, so it's a valid parent for the comment below.
|
|
124
136
|
const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id }, participant.create_session_headers()), CellCreateOutput);
|
|
@@ -127,7 +139,7 @@ export const describe_cell_gated_create_cross_tests = (options) => {
|
|
|
127
139
|
// `root_id !== parent_id`, so the handler must read the governing
|
|
128
140
|
// root (the space) *through* the post (the separate in-tx root read),
|
|
129
141
|
// and the authorizer must resolve `policy.comment` from the SPACE's
|
|
130
|
-
//
|
|
142
|
+
// policy, not the post's. This is the only case that exercises the
|
|
131
143
|
// deep-read branch on both spines.
|
|
132
144
|
const comment = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'comment', data: {}, parent_id: post.cell.id }, participant.create_session_headers()), CellCreateOutput);
|
|
133
145
|
assert.strictEqual(comment.cell.parent_id, post.cell.id, 'the immediate parent is the post');
|
|
@@ -137,7 +149,7 @@ export const describe_cell_gated_create_cross_tests = (options) => {
|
|
|
137
149
|
test_if(capabilities.cell_gated_create, 'a contribution under a soft-deleted root is 404 cell_not_found (the missing-parent shape)', async () => {
|
|
138
150
|
const fixture = await setup_test();
|
|
139
151
|
const admin_headers = fixture.create_session_headers();
|
|
140
|
-
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: TEST_SPACE_POLICY, visibility: 'public' }, admin_headers), CellCreateOutput);
|
|
152
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {}, policy: TEST_SPACE_POLICY, visibility: 'public' }, admin_headers), CellCreateOutput);
|
|
141
153
|
const participant = await fixture.create_account({
|
|
142
154
|
username: 'tombstone_participant',
|
|
143
155
|
roles: [PARTICIPATION_ROLE]
|
|
@@ -155,5 +167,89 @@ export const describe_cell_gated_create_cross_tests = (options) => {
|
|
|
155
167
|
const missing_parent = await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'comment', data: {}, parent_id: '00000000-0000-4000-8000-000000000000' }, participant_headers);
|
|
156
168
|
assert.deepStrictEqual(refused.error, missing_parent.error);
|
|
157
169
|
});
|
|
170
|
+
describe('cell root policy parity', () => {
|
|
171
|
+
test_if(capabilities.cell_gated_create, 'a root takes a policy at create and through cell_policy_set, audited old and new', async () => {
|
|
172
|
+
const fixture = await setup_test();
|
|
173
|
+
const admin_headers = fixture.create_session_headers();
|
|
174
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {}, policy: TEST_SPACE_POLICY, visibility: 'public' }, admin_headers), CellCreateOutput);
|
|
175
|
+
assert.deepStrictEqual(space.cell.policy, TEST_SPACE_POLICY, 'the create stores the policy');
|
|
176
|
+
// Any viewer reads it.
|
|
177
|
+
const stranger = await fixture.create_account({ username: 'policy_reader' });
|
|
178
|
+
const read = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_get', { id: space.cell.id }, stranger.create_session_headers()), CellGetOutput);
|
|
179
|
+
assert.deepStrictEqual(read.cell.policy, TEST_SPACE_POLICY);
|
|
180
|
+
const opened = { post: { min_role: PARTICIPATION_ROLE, moderation_required: false } };
|
|
181
|
+
const set = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_policy_set', { cell_id: space.cell.id, policy: opened }, admin_headers), CellPolicySetOutput);
|
|
182
|
+
assert.deepStrictEqual(set.cell.policy, opened);
|
|
183
|
+
const participant = await fixture.create_account({
|
|
184
|
+
username: 'policy_participant',
|
|
185
|
+
roles: [PARTICIPATION_ROLE]
|
|
186
|
+
});
|
|
187
|
+
const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: space.cell.id }, participant.create_session_headers()), CellCreateOutput);
|
|
188
|
+
assert.strictEqual(post.cell.moderation, 'approved', 'the new policy governs');
|
|
189
|
+
assert.strictEqual(post.cell.policy, null, 'a contribution carries no policy');
|
|
190
|
+
const cleared = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_policy_set', { cell_id: space.cell.id, policy: null }, admin_headers), CellPolicySetOutput);
|
|
191
|
+
assert.strictEqual(cleared.cell.policy, null);
|
|
192
|
+
const events = await list_audit_events(fixture, { event_type: 'cell_policy_set' }, rpc_path);
|
|
193
|
+
assert.deepStrictEqual(events.map((e) => e.metadata).reverse(), [
|
|
194
|
+
{ cell_id: space.cell.id, policy: opened, previous_policy: TEST_SPACE_POLICY },
|
|
195
|
+
{ cell_id: space.cell.id, policy: null, previous_policy: opened }
|
|
196
|
+
], 'each set is audited with the policy it wrote and the one it replaced');
|
|
197
|
+
});
|
|
198
|
+
test_if(capabilities.cell_gated_create, 'a policy is root-only and must pass the authorizer', async () => {
|
|
199
|
+
const fixture = await setup_test();
|
|
200
|
+
const admin_headers = fixture.create_session_headers();
|
|
201
|
+
const space = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {}, policy: TEST_SPACE_POLICY, visibility: 'public' }, admin_headers), CellCreateOutput);
|
|
202
|
+
const react = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'react', data: {}, parent_id: space.cell.id }, admin_headers), CellCreateOutput);
|
|
203
|
+
const on_contribution = await cross_rpc_call(fixture.transport, rpc_path, 'cell_policy_set', { cell_id: react.cell.id, policy: TEST_SPACE_POLICY }, admin_headers);
|
|
204
|
+
assert.ok(!on_contribution.ok);
|
|
205
|
+
assert.strictEqual(error_reason(on_contribution), 'cell_policy_root_only');
|
|
206
|
+
const beside_parent = await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'react', data: {}, parent_id: space.cell.id, policy: TEST_SPACE_POLICY }, admin_headers);
|
|
207
|
+
assert.ok(!beside_parent.ok);
|
|
208
|
+
assert.strictEqual(error_reason(beside_parent), 'cell_policy_root_only');
|
|
209
|
+
for (const bad of [
|
|
210
|
+
{ post: true },
|
|
211
|
+
{ post: { min_role: 1 } },
|
|
212
|
+
{ post: { other: true } }
|
|
213
|
+
]) {
|
|
214
|
+
const refused = await cross_rpc_call(fixture.transport, rpc_path, 'cell_policy_set', { cell_id: space.cell.id, policy: bad }, admin_headers);
|
|
215
|
+
assert.ok(!refused.ok, JSON.stringify(bad));
|
|
216
|
+
assert.strictEqual(error_reason(refused), 'cell_policy_invalid', JSON.stringify(bad));
|
|
217
|
+
const refused_create = await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: SPACE_CELL_KIND, data: {}, policy: bad }, admin_headers);
|
|
218
|
+
assert.strictEqual(error_reason(refused_create), 'cell_policy_invalid', JSON.stringify(bad));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
test_if(capabilities.cell_gated_create, "an editor-grant holder can't change a root's policy, through the verb or through data", async () => {
|
|
222
|
+
const fixture = await setup_test();
|
|
223
|
+
// A non-admin's root (a parentless non-`space` kind is open), its
|
|
224
|
+
// policy moderating posts.
|
|
225
|
+
const owner = await fixture.create_account({ username: 'policy_owner' });
|
|
226
|
+
const owner_headers = owner.create_session_headers();
|
|
227
|
+
const root = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'forum', data: {}, policy: TEST_SPACE_POLICY, visibility: 'public' }, owner_headers), CellCreateOutput);
|
|
228
|
+
const editor = await fixture.create_account({ username: 'policy_editor' });
|
|
229
|
+
expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_grant_create', {
|
|
230
|
+
cell_id: root.cell.id,
|
|
231
|
+
level: 'editor',
|
|
232
|
+
principal: { kind: 'actor', actor_id: editor.actor.id }
|
|
233
|
+
}, owner_headers), CellGrantCreateOutput);
|
|
234
|
+
const editor_headers = editor.create_session_headers();
|
|
235
|
+
const opened = { post: { min_role: PARTICIPATION_ROLE, moderation_required: false } };
|
|
236
|
+
const refused = await cross_rpc_call(fixture.transport, rpc_path, 'cell_policy_set', { cell_id: root.cell.id, policy: opened }, editor_headers);
|
|
237
|
+
assert.ok(!refused.ok, 'an editor must not set the policy');
|
|
238
|
+
assert.strictEqual(error_reason(refused), 'cell_policy_manage_only');
|
|
239
|
+
// The editor may rewrite `data` — a `data.policy` is just content.
|
|
240
|
+
const edited = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_update', { cell_id: root.cell.id, data: { policy: opened } }, editor_headers), CellUpdateOutput);
|
|
241
|
+
assert.deepStrictEqual(edited.cell.policy, TEST_SPACE_POLICY, 'a data write leaves the policy');
|
|
242
|
+
const participant = await fixture.create_account({
|
|
243
|
+
username: 'policy_poster',
|
|
244
|
+
roles: [PARTICIPATION_ROLE]
|
|
245
|
+
});
|
|
246
|
+
const post = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'post', data: {}, parent_id: root.cell.id }, participant.create_session_headers()), CellCreateOutput);
|
|
247
|
+
assert.strictEqual(post.cell.moderation, 'pending', "moderation follows the root's policy column, not its `data.policy`");
|
|
248
|
+
// Someone who can't view a private root gets the mask.
|
|
249
|
+
const hidden = expect_output(await cross_rpc_call(fixture.transport, rpc_path, 'cell_create', { kind: 'forum', data: {}, policy: TEST_SPACE_POLICY }, owner_headers), CellCreateOutput);
|
|
250
|
+
const masked = await cross_rpc_call(fixture.transport, rpc_path, 'cell_policy_set', { cell_id: hidden.cell.id, policy: opened }, editor_headers);
|
|
251
|
+
assert.strictEqual(error_reason(masked), 'cell_not_found');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
158
254
|
});
|
|
159
255
|
};
|
|
@@ -2,7 +2,8 @@ import '../assert_dev_env.js';
|
|
|
2
2
|
import type { RpcPathCapabilityGatedCrossSuiteOptions } from './setup.js';
|
|
3
3
|
/**
|
|
4
4
|
* Cross-backend parity for the **`cell_moderate` verb** (the `pending →
|
|
5
|
-
* approved | rejected`
|
|
5
|
+
* approved | rejected` decision and the `approved → rejected` takedown) —
|
|
6
|
+
* root-authority-gated.
|
|
6
7
|
*
|
|
7
8
|
* Builds a moderated public space (admin) + a `participant`'s `post` (born
|
|
8
9
|
* `pending` + private under the `moderation_required: true` policy), then
|
|
@@ -20,6 +21,11 @@ import type { RpcPathCapabilityGatedCrossSuiteOptions } from './setup.js';
|
|
|
20
21
|
* - **a cell with no governing root** → `400 cell_not_a_contribution` for any
|
|
21
22
|
* viewer (owner, admin), the cell left unmoderated; a non-viewer gets the
|
|
22
23
|
* `cell_not_found` mask first.
|
|
24
|
+
* - **approval needs a pending contribution, rejection a pending or approved
|
|
25
|
+
* one** → else `400 cell_not_pending`, the cell left as it was: a root owner
|
|
26
|
+
* a private, born-approved contribution was shared with can't publish it by
|
|
27
|
+
* "approving" it; an approved post is not approved again but can be taken
|
|
28
|
+
* down (rejected, visibility untouched); a rejected post is final.
|
|
23
29
|
*
|
|
24
30
|
* And that moderation gates reads — a cell's effective audience is its
|
|
25
31
|
* visibility or grants **and** its moderation admission (unmoderated or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_moderate.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_moderate.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA+B9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAQ1E
|
|
1
|
+
{"version":3,"file":"cell_moderate.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_moderate.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA+B9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAQ1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AACH,eAAO,MAAM,kCAAkC,GAC9C,SAAS,uCAAuC,KAC9C,IAunCF,CAAC"}
|