@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.
Files changed (210) hide show
  1. package/dist/actions/CLAUDE.md +11 -9
  2. package/dist/actions/action_bridge.d.ts +7 -0
  3. package/dist/actions/action_bridge.d.ts.map +1 -1
  4. package/dist/actions/action_bridge.js +10 -0
  5. package/dist/actions/action_event.d.ts.map +1 -1
  6. package/dist/actions/action_event.js +14 -1
  7. package/dist/actions/action_event_types.d.ts +1 -1
  8. package/dist/actions/action_event_types.d.ts.map +1 -1
  9. package/dist/actions/perform_action.d.ts +17 -1
  10. package/dist/actions/perform_action.d.ts.map +1 -1
  11. package/dist/actions/perform_action.js +30 -7
  12. package/dist/actions/register_action_ws.d.ts +8 -2
  13. package/dist/actions/register_action_ws.d.ts.map +1 -1
  14. package/dist/actions/register_action_ws.js +24 -4
  15. package/dist/actions/transports_ws_auth_guard.d.ts +6 -4
  16. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  17. package/dist/actions/transports_ws_auth_guard.js +9 -6
  18. package/dist/actions/ws_endpoint_spec.d.ts +1 -1
  19. package/dist/auth/CLAUDE.md +72 -33
  20. package/dist/auth/account_action_specs.d.ts +12 -0
  21. package/dist/auth/account_action_specs.d.ts.map +1 -1
  22. package/dist/auth/account_action_specs.js +16 -2
  23. package/dist/auth/account_actions.d.ts.map +1 -1
  24. package/dist/auth/account_actions.js +7 -4
  25. package/dist/auth/actor_lookup_queries.d.ts +3 -2
  26. package/dist/auth/actor_lookup_queries.d.ts.map +1 -1
  27. package/dist/auth/actor_lookup_queries.js +6 -3
  28. package/dist/auth/actor_search_queries.d.ts +5 -2
  29. package/dist/auth/actor_search_queries.d.ts.map +1 -1
  30. package/dist/auth/actor_search_queries.js +11 -4
  31. package/dist/auth/admin_action_specs.d.ts +4 -4
  32. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  33. package/dist/auth/admin_action_specs.js +33 -12
  34. package/dist/auth/admin_actions.d.ts.map +1 -1
  35. package/dist/auth/admin_actions.js +35 -6
  36. package/dist/auth/all_cell_actions.d.ts +11 -2
  37. package/dist/auth/all_cell_actions.d.ts.map +1 -1
  38. package/dist/auth/all_cell_actions.js +2 -1
  39. package/dist/auth/audit_emitter.d.ts +23 -3
  40. package/dist/auth/audit_emitter.d.ts.map +1 -1
  41. package/dist/auth/audit_emitter.js +14 -4
  42. package/dist/auth/audit_log_schema.d.ts +1 -0
  43. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  44. package/dist/auth/audit_log_schema.js +9 -5
  45. package/dist/auth/bootstrap_account.d.ts +25 -1
  46. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  47. package/dist/auth/bootstrap_account.js +37 -4
  48. package/dist/auth/bootstrap_routes.d.ts +3 -2
  49. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  50. package/dist/auth/bootstrap_routes.js +5 -4
  51. package/dist/auth/cell_action_specs.d.ts +215 -15
  52. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  53. package/dist/auth/cell_action_specs.js +103 -16
  54. package/dist/auth/cell_actions.d.ts +40 -18
  55. package/dist/auth/cell_actions.d.ts.map +1 -1
  56. package/dist/auth/cell_actions.js +126 -19
  57. package/dist/auth/cell_audit_events.d.ts +1 -1
  58. package/dist/auth/cell_audit_events.d.ts.map +1 -1
  59. package/dist/auth/cell_audit_events.js +3 -2
  60. package/dist/auth/cell_audit_metadata.d.ts +11 -0
  61. package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
  62. package/dist/auth/cell_audit_metadata.js +10 -0
  63. package/dist/auth/cell_grant_action_specs.d.ts +7 -0
  64. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
  65. package/dist/auth/cell_grant_action_specs.js +7 -0
  66. package/dist/auth/cell_grant_actions.d.ts +36 -4
  67. package/dist/auth/cell_grant_actions.d.ts.map +1 -1
  68. package/dist/auth/cell_grant_actions.js +20 -3
  69. package/dist/auth/credential_type_schema.d.ts +3 -2
  70. package/dist/auth/credential_type_schema.d.ts.map +1 -1
  71. package/dist/auth/credential_type_schema.js +3 -2
  72. package/dist/auth/role_grant_offer_action_specs.d.ts +18 -2
  73. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  74. package/dist/auth/role_grant_offer_action_specs.js +30 -6
  75. package/dist/auth/role_grant_offer_actions.d.ts +9 -4
  76. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  77. package/dist/auth/role_grant_offer_actions.js +58 -7
  78. package/dist/auth/role_grant_offer_notifications.d.ts +3 -2
  79. package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -1
  80. package/dist/auth/role_grant_offer_notifications.js +3 -2
  81. package/dist/auth/role_grant_offer_queries.d.ts +20 -0
  82. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  83. package/dist/auth/role_grant_offer_queries.js +35 -0
  84. package/dist/auth/role_grant_queries.d.ts +27 -9
  85. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  86. package/dist/auth/role_grant_queries.js +40 -13
  87. package/dist/auth/role_schema.d.ts +14 -8
  88. package/dist/auth/role_schema.d.ts.map +1 -1
  89. package/dist/auth/role_schema.js +6 -3
  90. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  91. package/dist/auth/self_service_role_actions.js +20 -0
  92. package/dist/db/CLAUDE.md +9 -2
  93. package/dist/db/cell_ddl.d.ts +9 -1
  94. package/dist/db/cell_ddl.d.ts.map +1 -1
  95. package/dist/db/cell_ddl.js +11 -1
  96. package/dist/db/cell_queries.d.ts +53 -7
  97. package/dist/db/cell_queries.d.ts.map +1 -1
  98. package/dist/db/cell_queries.js +61 -7
  99. package/dist/http/CLAUDE.md +14 -2
  100. package/dist/http/body_limit.d.ts +3 -1
  101. package/dist/http/body_limit.d.ts.map +1 -1
  102. package/dist/http/body_limit.js +6 -1
  103. package/dist/http/error_schemas.d.ts +8 -0
  104. package/dist/http/error_schemas.d.ts.map +1 -1
  105. package/dist/http/error_schemas.js +12 -0
  106. package/dist/http/jsonrpc.d.ts +9 -0
  107. package/dist/http/jsonrpc.d.ts.map +1 -1
  108. package/dist/http/jsonrpc.js +17 -0
  109. package/dist/http/jsonrpc_errors.d.ts +40 -15
  110. package/dist/http/jsonrpc_errors.d.ts.map +1 -1
  111. package/dist/http/jsonrpc_errors.js +99 -35
  112. package/dist/http/proxy.d.ts +4 -2
  113. package/dist/http/proxy.d.ts.map +1 -1
  114. package/dist/http/proxy.js +7 -2
  115. package/dist/http/surface.d.ts +3 -1
  116. package/dist/http/surface.d.ts.map +1 -1
  117. package/dist/http/surface.js +7 -4
  118. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  119. package/dist/realtime/sse_auth_guard.js +3 -2
  120. package/dist/schema_meta.d.ts +92 -1
  121. package/dist/schema_meta.d.ts.map +1 -1
  122. package/dist/schema_meta.js +117 -0
  123. package/dist/server/app_server.d.ts +13 -5
  124. package/dist/server/app_server.d.ts.map +1 -1
  125. package/dist/server/app_server.js +32 -8
  126. package/dist/testing/CLAUDE.md +196 -82
  127. package/dist/testing/adversarial_input_cases.d.ts +67 -7
  128. package/dist/testing/adversarial_input_cases.d.ts.map +1 -1
  129. package/dist/testing/adversarial_input_cases.js +210 -165
  130. package/dist/testing/app_server.d.ts +3 -1
  131. package/dist/testing/app_server.d.ts.map +1 -1
  132. package/dist/testing/audit_drift_guard.d.ts +3 -2
  133. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  134. package/dist/testing/audit_drift_guard.js +5 -3
  135. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/actor_lookup.js +27 -0
  137. package/dist/testing/cross_backend/audit_cross_helpers.d.ts +6 -0
  138. package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -1
  139. package/dist/testing/cross_backend/audit_cross_helpers.js +11 -0
  140. package/dist/testing/cross_backend/capabilities.d.ts +29 -5
  141. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  142. package/dist/testing/cross_backend/capabilities.js +10 -6
  143. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  144. package/dist/testing/cross_backend/cell_crud.js +117 -2
  145. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  146. package/dist/testing/cross_backend/cell_gated_create.js +109 -13
  147. package/dist/testing/cross_backend/cell_moderate.d.ts +7 -1
  148. package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/cell_moderate.js +68 -4
  150. package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/cell_ref_admission.js +18 -9
  152. package/dist/testing/cross_backend/conformance_table.d.ts +5 -0
  153. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  154. package/dist/testing/cross_backend/conformance_table.js +12 -10
  155. package/dist/testing/cross_backend/cross_rpc.d.ts +3 -1
  156. package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -1
  157. package/dist/testing/cross_backend/cross_rpc.js +2 -2
  158. package/dist/testing/cross_backend/default_backend_configs.d.ts +9 -7
  159. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  160. package/dist/testing/cross_backend/default_backend_configs.js +3 -3
  161. package/dist/testing/cross_backend/differential.d.ts +7 -0
  162. package/dist/testing/cross_backend/differential.d.ts.map +1 -1
  163. package/dist/testing/cross_backend/differential.js +17 -0
  164. package/dist/testing/cross_backend/expected_schema.json +1 -0
  165. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  166. package/dist/testing/cross_backend/full_spine_mount.js +5 -4
  167. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -1
  168. package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +74 -2
  169. package/dist/testing/cross_backend/rpc_adversarial.d.ts +9 -0
  170. package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -1
  171. package/dist/testing/cross_backend/rpc_adversarial.js +16 -6
  172. package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +1 -1
  173. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  174. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
  175. package/dist/testing/cross_backend/setup.d.ts +2 -2
  176. package/dist/testing/cross_backend/spine_suites.d.ts +85 -5
  177. package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -1
  178. package/dist/testing/cross_backend/spine_suites.js +291 -20
  179. package/dist/testing/cross_backend/spine_surface_constants.d.ts +15 -0
  180. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  181. package/dist/testing/cross_backend/spine_surface_constants.js +24 -0
  182. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +18 -19
  183. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  184. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +66 -42
  185. package/dist/testing/cross_backend/testing_backdoor.d.ts +27 -4
  186. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  187. package/dist/testing/cross_backend/testing_backdoor.js +136 -41
  188. package/dist/testing/cross_backend/testing_reset_actions.d.ts +30 -15
  189. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  190. package/dist/testing/cross_backend/testing_reset_actions.js +52 -35
  191. package/dist/testing/cross_backend/transport_composition.js +3 -3
  192. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  193. package/dist/testing/cross_backend/ts_spine_backend_config.js +6 -3
  194. package/dist/testing/rpc_attack_surface.d.ts +10 -0
  195. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  196. package/dist/testing/rpc_attack_surface.js +15 -3
  197. package/dist/testing/rpc_envelope_cases.d.ts +5 -2
  198. package/dist/testing/rpc_envelope_cases.d.ts.map +1 -1
  199. package/dist/testing/rpc_envelope_cases.js +4 -1
  200. package/dist/testing/rpc_round_trip.js +3 -3
  201. package/dist/testing/schema_generators.d.ts +99 -2
  202. package/dist/testing/schema_generators.d.ts.map +1 -1
  203. package/dist/testing/schema_generators.js +244 -76
  204. package/dist/testing/stubs.d.ts +4 -3
  205. package/dist/testing/stubs.d.ts.map +1 -1
  206. package/dist/testing/stubs.js +6 -4
  207. package/dist/zod.d.ts +9 -0
  208. package/dist/zod.d.ts.map +1 -1
  209. package/dist/zod.js +20 -0
  210. 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;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC/B,SAAS,gBAAgB,EACzB,WAAU,MAAuB,KAC/B,OAAO,CAAC,IAAI,CASd,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"}
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 registers the `_testing_put_fact` seeder. Gates
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`, and the fact cases of the
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, which
207
- * each backend config states itself. Spread one and add every mount flag:
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;AAiD9B;;;;;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;;;;;;;;;;OAUG;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;CAClC;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;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;AAE7E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAyBpC,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"}
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: the family defaults are
32
- * `FamilyDefaultCapabilities`, which omits them, so every backend config states
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). A wiring fact no suite
42
- * reads is not a capability.
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;AAiE9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E,eAAO,MAAM,8BAA8B,GAC1C,SAAS,uCAAuC,KAC9C,IA6lBF,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 { CELL_KIND_LENGTH_MAX, CellCreateOutput, CellDeleteOutput, CellGetOutput, CellListOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
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;AAsD9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAQ1E,eAAO,MAAM,sCAAsC,GAClD,SAAS,uCAAuC,KAC9C,IAuTF,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 `data.policy[kind] = {min_role, moderation_required}`,
18
- * with the moderation outcome folded into the verdict; plain parentless creates
19
- * stay open. The suite asserts all spines agree on:
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 as
121
- // `root_data`. The post is gated `moderation_required: true`, so it's
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
- // data, not the post's. This is the only case that exercises the
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` transition) — root-authority-gated.
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,eAAO,MAAM,kCAAkC,GAC9C,SAAS,uCAAuC,KAC9C,IA69BF,CAAC"}
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"}