@fuzdev/fuz_app 0.146.1 → 0.148.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/dist/actions/CLAUDE.md +6 -4
  2. package/dist/actions/action_codegen.d.ts +15 -1
  3. package/dist/actions/action_codegen.d.ts.map +1 -1
  4. package/dist/actions/action_codegen.js +23 -2
  5. package/dist/actions/perform_action.js +1 -1
  6. package/dist/auth/CLAUDE.md +1 -1
  7. package/dist/auth/bootstrap_account.d.ts +2 -1
  8. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  9. package/dist/auth/bootstrap_account.js +17 -6
  10. package/dist/auth/cell_action_specs.d.ts +3 -1
  11. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  12. package/dist/auth/cell_action_specs.js +5 -5
  13. package/dist/auth/cell_actions.d.ts.map +1 -1
  14. package/dist/auth/cell_actions.js +2 -3
  15. package/dist/auth/cell_data_schema.d.ts +1 -1
  16. package/dist/auth/cell_data_schema.js +1 -1
  17. package/dist/auth/cell_field_action_specs.d.ts +4 -3
  18. package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
  19. package/dist/auth/cell_field_action_specs.js +5 -6
  20. package/dist/auth/cell_field_actions.d.ts.map +1 -1
  21. package/dist/auth/cell_field_actions.js +8 -1
  22. package/dist/auth/cell_item_action_specs.d.ts +4 -2
  23. package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
  24. package/dist/auth/cell_item_action_specs.js +5 -5
  25. package/dist/auth/cell_item_actions.d.ts.map +1 -1
  26. package/dist/auth/cell_item_actions.js +8 -1
  27. package/dist/auth/keyring.d.ts +35 -38
  28. package/dist/auth/keyring.d.ts.map +1 -1
  29. package/dist/auth/keyring.js +43 -50
  30. package/dist/auth/password_argon2.d.ts.map +1 -1
  31. package/dist/auth/password_argon2.js +11 -8
  32. package/dist/auth/role_grant_offer_action_specs.d.ts +2 -0
  33. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  34. package/dist/auth/role_grant_offer_action_specs.js +3 -1
  35. package/dist/auth/session_cookie.d.ts.map +1 -1
  36. package/dist/auth/session_cookie.js +5 -2
  37. package/dist/db/CLAUDE.md +18 -16
  38. package/dist/db/cell_queries.d.ts +3 -2
  39. package/dist/db/cell_queries.d.ts.map +1 -1
  40. package/dist/db/cell_queries.js +3 -2
  41. package/dist/db/fact_disk_storage.d.ts +15 -15
  42. package/dist/db/fact_disk_storage.d.ts.map +1 -1
  43. package/dist/db/fact_disk_storage.js +22 -28
  44. package/dist/db/fact_queries.d.ts +2 -3
  45. package/dist/db/fact_queries.d.ts.map +1 -1
  46. package/dist/db/fact_queries.js +2 -3
  47. package/dist/db/fact_store.d.ts +20 -18
  48. package/dist/db/fact_store.d.ts.map +1 -1
  49. package/dist/db/fact_store.js +30 -44
  50. package/dist/db/file_fact_url.d.ts +19 -4
  51. package/dist/db/file_fact_url.d.ts.map +1 -1
  52. package/dist/db/file_fact_url.js +19 -4
  53. package/dist/primitive_schemas.d.ts +1 -1
  54. package/dist/primitive_schemas.d.ts.map +1 -1
  55. package/dist/primitive_schemas.js +2 -1
  56. package/dist/schema_meta.d.ts +20 -0
  57. package/dist/schema_meta.d.ts.map +1 -1
  58. package/dist/schema_meta.js +7 -1
  59. package/dist/server/env.d.ts.map +1 -1
  60. package/dist/server/env.js +2 -2
  61. package/dist/server/serve_fact_route.d.ts +9 -5
  62. package/dist/server/serve_fact_route.d.ts.map +1 -1
  63. package/dist/server/serve_fact_route.js +15 -7
  64. package/dist/testing/CLAUDE.md +737 -114
  65. package/dist/testing/admin_integration.d.ts.map +1 -1
  66. package/dist/testing/admin_integration.js +73 -5
  67. package/dist/testing/adversarial_input.d.ts +0 -47
  68. package/dist/testing/adversarial_input.d.ts.map +1 -1
  69. package/dist/testing/adversarial_input.js +4 -340
  70. package/dist/testing/adversarial_input_cases.d.ts +64 -0
  71. package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
  72. package/dist/testing/adversarial_input_cases.js +348 -0
  73. package/dist/testing/app_server.d.ts.map +1 -1
  74. package/dist/testing/app_server.js +2 -2
  75. package/dist/testing/audit_completeness.d.ts.map +1 -1
  76. package/dist/testing/audit_completeness.js +24 -20
  77. package/dist/testing/audit_drift_guard.d.ts +27 -1
  78. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  79. package/dist/testing/audit_drift_guard.js +50 -0
  80. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  81. package/dist/testing/cross_backend/account_lifecycle.js +29 -12
  82. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  83. package/dist/testing/cross_backend/actor_lookup.js +7 -25
  84. package/dist/testing/cross_backend/app_settings.d.ts.map +1 -1
  85. package/dist/testing/cross_backend/app_settings.js +4 -6
  86. package/dist/testing/cross_backend/audit_cross_helpers.d.ts +51 -0
  87. package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
  88. package/dist/testing/cross_backend/audit_cross_helpers.js +78 -0
  89. package/dist/testing/cross_backend/backend_config.d.ts +2 -1
  90. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -1
  91. package/dist/testing/cross_backend/body_size.d.ts +6 -0
  92. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  93. package/dist/testing/cross_backend/body_size.js +26 -44
  94. package/dist/testing/cross_backend/body_size_smuggling.d.ts +5 -0
  95. package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
  96. package/dist/testing/cross_backend/body_size_smuggling.js +7 -14
  97. package/dist/testing/cross_backend/bootstrap_backend.d.ts +11 -1
  98. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -1
  99. package/dist/testing/cross_backend/bootstrap_backend.js +2 -1
  100. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +3 -1
  101. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -1
  102. package/dist/testing/cross_backend/capabilities.d.ts +55 -59
  103. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  104. package/dist/testing/cross_backend/capabilities.js +20 -23
  105. package/dist/testing/cross_backend/cargo_binary.d.ts +70 -0
  106. package/dist/testing/cross_backend/cargo_binary.d.ts.map +1 -0
  107. package/dist/testing/cross_backend/cargo_binary.js +148 -0
  108. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  109. package/dist/testing/cross_backend/cell_crud.js +57 -1
  110. package/dist/testing/cross_backend/cell_gated_create.d.ts +0 -60
  111. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  112. package/dist/testing/cross_backend/cell_gated_create.js +9 -487
  113. package/dist/testing/cross_backend/cell_grant_role.d.ts +14 -2
  114. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  115. package/dist/testing/cross_backend/cell_grant_role.js +22 -12
  116. package/dist/testing/cross_backend/cell_moderate.d.ts +66 -0
  117. package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -0
  118. package/dist/testing/cross_backend/cell_moderate.js +511 -0
  119. package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
  120. package/dist/testing/cross_backend/cell_ref_admission.js +1 -1
  121. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  122. package/dist/testing/cross_backend/cell_relations.js +40 -19
  123. package/dist/testing/cross_backend/conformance_case.d.ts +43 -4
  124. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  125. package/dist/testing/cross_backend/conformance_case.js +29 -1
  126. package/dist/testing/cross_backend/conformance_rpc_auth.d.ts +93 -0
  127. package/dist/testing/cross_backend/conformance_rpc_auth.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/conformance_rpc_auth.js +125 -0
  129. package/dist/testing/cross_backend/conformance_table.d.ts +144 -4
  130. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  131. package/dist/testing/cross_backend/conformance_table.js +297 -65
  132. package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -1
  133. package/dist/testing/cross_backend/cookie_attributes.js +3 -2
  134. package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +38 -5
  135. package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/create_cross_backend_global_setup.js +24 -3
  137. package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +5 -5
  138. package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts.map +1 -1
  139. package/dist/testing/cross_backend/credential_deadline.d.ts +10 -6
  140. package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -1
  141. package/dist/testing/cross_backend/credential_deadline.js +32 -31
  142. package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
  143. package/dist/testing/cross_backend/credential_header_robustness.js +4 -6
  144. package/dist/testing/cross_backend/{cell_cross_helpers.d.ts → cross_rpc.d.ts} +12 -2
  145. package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/{cell_cross_helpers.js → cross_rpc.js} +31 -7
  147. package/dist/testing/cross_backend/default_backend_configs.d.ts +56 -61
  148. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/default_backend_configs.js +97 -91
  150. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/default_spine_surface.js +2 -2
  152. package/dist/testing/cross_backend/differential.d.ts +126 -0
  153. package/dist/testing/cross_backend/differential.d.ts.map +1 -0
  154. package/dist/testing/cross_backend/differential.js +201 -0
  155. package/dist/testing/cross_backend/differential_normalize.d.ts +150 -0
  156. package/dist/testing/cross_backend/differential_normalize.d.ts.map +1 -0
  157. package/dist/testing/cross_backend/differential_normalize.js +153 -0
  158. package/dist/testing/cross_backend/fact_serving.d.ts +5 -0
  159. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  160. package/dist/testing/cross_backend/fact_serving.js +41 -7
  161. package/dist/testing/cross_backend/full_spine_mount.d.ts +15 -0
  162. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  163. package/dist/testing/cross_backend/full_spine_mount.js +55 -1
  164. package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
  165. package/dist/testing/cross_backend/identity_parity.js +6 -5
  166. package/dist/testing/cross_backend/in_process_setup.d.ts +12 -2
  167. package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
  168. package/dist/testing/cross_backend/in_process_setup.js +10 -7
  169. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  170. package/dist/testing/cross_backend/login_security.js +13 -18
  171. package/dist/testing/cross_backend/origin.js +1 -1
  172. package/dist/testing/cross_backend/origin_conformance.js +1 -1
  173. package/dist/testing/cross_backend/origin_scope.d.ts +6 -0
  174. package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -1
  175. package/dist/testing/cross_backend/origin_scope.js +13 -12
  176. package/dist/testing/cross_backend/peer_ping_ws.d.ts +2 -5
  177. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  178. package/dist/testing/cross_backend/peer_ping_ws.js +14 -24
  179. package/dist/testing/cross_backend/postgres_database.d.ts +85 -0
  180. package/dist/testing/cross_backend/postgres_database.d.ts.map +1 -0
  181. package/dist/testing/cross_backend/postgres_database.js +140 -0
  182. package/dist/testing/cross_backend/provided_context.d.ts +28 -0
  183. package/dist/testing/cross_backend/provided_context.d.ts.map +1 -0
  184. package/dist/testing/cross_backend/provided_context.js +35 -0
  185. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
  186. package/dist/testing/cross_backend/query_shape.js +10 -12
  187. package/dist/testing/cross_backend/ready.js +2 -2
  188. package/dist/testing/cross_backend/revocation_drivers.d.ts +4 -3
  189. package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -1
  190. package/dist/testing/cross_backend/revocation_drivers.js +1 -1
  191. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +3 -2
  192. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -1
  193. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts +13 -0
  194. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -0
  195. package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +203 -0
  196. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +2 -5
  197. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  198. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +203 -111
  199. package/dist/testing/cross_backend/role_grant_participation.d.ts +13 -0
  200. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
  201. package/dist/testing/cross_backend/role_grant_participation.js +32 -21
  202. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -1
  203. package/dist/testing/cross_backend/role_grant_role_grammar.js +6 -9
  204. package/dist/testing/cross_backend/rpc_adversarial.d.ts +38 -0
  205. package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
  206. package/dist/testing/cross_backend/rpc_adversarial.js +292 -0
  207. package/dist/testing/cross_backend/rpc_success_fixtures.d.ts +24 -0
  208. package/dist/testing/cross_backend/rpc_success_fixtures.d.ts.map +1 -0
  209. package/dist/testing/cross_backend/rpc_success_fixtures.js +124 -0
  210. package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +101 -0
  211. package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts.map +1 -0
  212. package/dist/testing/cross_backend/rust_spine_consumer_surface.js +113 -0
  213. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +10 -0
  214. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  215. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +27 -18
  216. package/dist/testing/cross_backend/session_cap.d.ts +9 -8
  217. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  218. package/dist/testing/cross_backend/session_cap.js +9 -4
  219. package/dist/testing/cross_backend/session_cookie_codec.d.ts +19 -0
  220. package/dist/testing/cross_backend/session_cookie_codec.d.ts.map +1 -0
  221. package/dist/testing/cross_backend/session_cookie_codec.js +128 -0
  222. package/dist/testing/cross_backend/setup.d.ts +56 -54
  223. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  224. package/dist/testing/cross_backend/setup.js +29 -29
  225. package/dist/testing/cross_backend/simple_request.d.ts.map +1 -1
  226. package/dist/testing/cross_backend/simple_request.js +4 -4
  227. package/dist/testing/cross_backend/spawn_backend.d.ts +59 -10
  228. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
  229. package/dist/testing/cross_backend/spawn_backend.js +219 -60
  230. package/dist/testing/cross_backend/spine_suites.d.ts +231 -0
  231. package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
  232. package/dist/testing/cross_backend/spine_suites.js +626 -0
  233. package/dist/testing/cross_backend/spine_surface_constants.d.ts +29 -0
  234. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  235. package/dist/testing/cross_backend/spine_surface_constants.js +29 -0
  236. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  237. package/dist/testing/cross_backend/sse_round_trip.js +3 -4
  238. package/dist/testing/cross_backend/standard.d.ts +18 -1
  239. package/dist/testing/cross_backend/standard.d.ts.map +1 -1
  240. package/dist/testing/cross_backend/standard.js +2 -0
  241. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +24 -0
  242. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  243. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +15 -0
  244. package/dist/testing/cross_backend/testing_backdoor.d.ts +2 -8
  245. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  246. package/dist/testing/cross_backend/testing_backdoor.js +7 -10
  247. package/dist/testing/cross_backend/testing_reset_actions.d.ts +10 -5
  248. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  249. package/dist/testing/cross_backend/testing_reset_actions.js +21 -12
  250. package/dist/testing/cross_backend/testing_server_bun.js +1 -1
  251. package/dist/testing/cross_backend/token_lifetime.js +1 -1
  252. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  253. package/dist/testing/cross_backend/token_scope_surface.js +9 -13
  254. package/dist/testing/cross_backend/transport_composition.d.ts +26 -2
  255. package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -1
  256. package/dist/testing/cross_backend/transport_composition.js +81 -51
  257. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +8 -0
  258. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  259. package/dist/testing/cross_backend/ts_spine_backend_config.js +17 -10
  260. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +2 -5
  261. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
  262. package/dist/testing/cross_backend/ws_action_rate_limit.js +8 -14
  263. package/dist/testing/cross_backend/ws_connection_cap.d.ts +4 -9
  264. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -1
  265. package/dist/testing/cross_backend/ws_connection_cap.js +11 -12
  266. package/dist/testing/cross_backend/ws_endpoint.d.ts +52 -0
  267. package/dist/testing/cross_backend/ws_endpoint.d.ts.map +1 -0
  268. package/dist/testing/cross_backend/ws_endpoint.js +54 -0
  269. package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +2 -5
  270. package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -1
  271. package/dist/testing/cross_backend/ws_inbound_dispatch.js +9 -15
  272. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +2 -7
  273. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
  274. package/dist/testing/cross_backend/ws_outbound_overflow.js +5 -9
  275. package/dist/testing/cross_backend/ws_role_gated.d.ts +4 -5
  276. package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -1
  277. package/dist/testing/cross_backend/ws_role_gated.js +12 -21
  278. package/dist/testing/cross_backend/ws_round_trip.d.ts +12 -9
  279. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  280. package/dist/testing/cross_backend/ws_round_trip.js +29 -34
  281. package/dist/testing/db_entities.d.ts +16 -3
  282. package/dist/testing/db_entities.d.ts.map +1 -1
  283. package/dist/testing/db_entities.js +28 -5
  284. package/dist/testing/integration_helpers.d.ts +61 -2
  285. package/dist/testing/integration_helpers.d.ts.map +1 -1
  286. package/dist/testing/integration_helpers.js +75 -18
  287. package/dist/testing/round_trip.d.ts +17 -0
  288. package/dist/testing/round_trip.d.ts.map +1 -1
  289. package/dist/testing/round_trip.js +67 -24
  290. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  291. package/dist/testing/rpc_attack_surface.js +13 -138
  292. package/dist/testing/rpc_envelope_cases.d.ts +111 -0
  293. package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
  294. package/dist/testing/rpc_envelope_cases.js +240 -0
  295. package/dist/testing/rpc_helpers.d.ts +31 -0
  296. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  297. package/dist/testing/rpc_helpers.js +35 -14
  298. package/dist/testing/rpc_round_trip.d.ts +62 -6
  299. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  300. package/dist/testing/rpc_round_trip.js +151 -39
  301. package/dist/testing/schema_generators.d.ts.map +1 -1
  302. package/dist/testing/schema_generators.js +22 -2
  303. package/dist/testing/standard.d.ts +26 -0
  304. package/dist/testing/standard.d.ts.map +1 -1
  305. package/dist/testing/standard.js +4 -1
  306. package/dist/testing/transports/raw_http.d.ts +21 -0
  307. package/dist/testing/transports/raw_http.d.ts.map +1 -1
  308. package/dist/testing/transports/raw_http.js +30 -0
  309. package/package.json +10 -8
  310. package/dist/testing/cross_backend/actor_search.d.ts +0 -6
  311. package/dist/testing/cross_backend/actor_search.d.ts.map +0 -1
  312. package/dist/testing/cross_backend/actor_search.js +0 -90
  313. package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +0 -1
@@ -79,6 +79,15 @@ across the auth test suite. Use for query-level tests needing real DB rows
79
79
  but not a full session/token bundle. For tests also needing an API token +
80
80
  session cookie + role_grants, use `bootstrap_test_keeper` from `app_server.ts`.
81
81
 
82
+ `create_test_extra_actor(db, account_id, name)` adds an actor to an
83
+ account — the multi-actor edge — with `created_at` read from the wall clock
84
+ (`clock_timestamp()`), not the transaction-fixed `NOW()` default. Actor lists
85
+ sort `created_at, id`, so a shared `created_at` would order them by random id;
86
+ seeded through it, the account's first actor sorts first and the extras in
87
+ insert order. `_testing_reset` and the in-process setup seed `extra_actors`
88
+ with it, as the Rust reset does with `query_create_actor_with_clock_timestamp`,
89
+ so both spines list a multi-actor keeper's actors in one order.
90
+
82
91
  `create_test_role_grant_direct(db, input)` wraps `query_create_role_grant`
83
92
  for tests needing an active role_grant seeded directly, bypassing the
84
93
  production offer/accept consent flow. Use only when the test focuses on
@@ -103,6 +112,7 @@ mismatch.
103
112
  ### `audit_drift_guard.ts` — audit-emission validation
104
113
 
105
114
  - `install_audit_drift_guard()` — `beforeEach` resets + `afterEach` zero-checks the `audit_metadata_validation_failures` + `audit_unknown_event_type_failures` counters from `auth/audit_log_queries.ts` and the emitter's write-failure counter (`get_audit_write_failures` in `auth/audit_emitter.ts`). Call once at the top of any `describe_db` block firing audit emits — production validation and the emitter's write are fail-open, so without this any regression shipping a typo'd `event_type` or undeclared metadata field, or losing a row the database refused, is silent. A test that fails a write on purpose resets the counter (`reset_audit_write_failures`) once it has asserted it.
115
+ - `assert_audit_event_metadata(event, {reject_unknown_keys?})` — the wire-side twin: parses a read audit row's `metadata` against its builtin event type's `audit_metadata_schemas` entry as declared, so a mistyped, `null`, or missing field fails while a consumer-added key passes (the schemas are `z.looseObject`, and a builtin's schema can't be overridden, so added keys are a consumer's only extension path). `reject_unknown_keys` closes the world: an undeclared key fails too, for a suite whose backend adds none. The wire schema leaves `metadata` a loose record and only the TS spine validates it, at insert and fail-open, so this is what holds every spine's rows to their event's contract. A consumer-registered event type passes unexamined. `list_audit_events` and the admin suite run it on every row they read, with `reject_unknown_keys` from the fixture's `reject_unknown_audit_metadata_keys` (a setup option on `default_cross_process_setup`, `default_in_process_setup` and `default_in_process_suite_options`); fuz_app's own spine wiring turns it on.
106
116
  - `assert_no_audit_drift()` — the zero-check itself, for a test that manages the counters. Pair with `await_pending_effects: true` (the `create_test_app` default) so fire-and-forget audit writes complete by response time.
107
117
  - `create_recording_audit_emitter(calls_ref?)` — no-op `AuditEmitter` pushing every `emit` and `emit_pool` call into `calls`. Pass `calls_ref` to write into a caller-owned array; omit to let the helper allocate. Returns `{emitter, calls}` — destructure `emitter` as the `audit` dep and read `calls` to assert. `add_listener` records (never fires) and returns a remover with the production semantics, so `listener_count` tracks what a server registered and released. Replaces per-file capturing emitters previously duplicated across `password_change.test.ts`, `audit_log.test.ts`, etc.
108
118
  - `RecordingAuditEmitter` — `{emitter: AuditEmitter; calls: Array<AuditLogInput>}`.
@@ -154,7 +164,7 @@ Key module-scope values:
154
164
 
155
165
  - `stub_password_deps` — `PasswordHashDeps` hashing via `stub_hash_${password}` and verifying by equality. Deterministic, no Argon2 cost — use for every test not specifically exercising password hashing.
156
166
  - `create_loopback_app_server_options()` — the loopback `create_app_server` options an in-process test server takes (`localhost` origins, the loopback proxy trusted, an empty env schema), fresh per call; `create_test_app` and `create_test_app_for_bootstrap` spread it under their own defaults (`rate_limiters: 'disabled_for_testing'`, `await_pending_effects: true`).
157
- - `TEST_COOKIE_SECRET` — 64-hex-char deterministic cookie secret. Produces a valid `Keyring` via `create_validated_keyring`. Never used in production — the stub guard plus fixed value is the contract.
167
+ - `TEST_COOKIE_SECRET` — 64-hex-char deterministic cookie secret. Produces a valid `Keyring` via `create_keyring`. Never used in production — the stub guard plus fixed value is the contract.
158
168
  - `fallback_db_factory` — module-level auth-only default factory (`create_default_db_factory`) `create_test_app_server` uses when no `db` is passed, reusing its driver's WASM cache. When `migration_namespaces` is supplied, a memoized sibling factory (keyed by namespace set) migrating `[auth_migration_ns, ...migration_namespaces]` is used instead — same shared WASM, extra tables.
159
169
 
160
170
  Two helpers share the "insert account + actor + roles + API token + session +
@@ -295,6 +305,87 @@ assert_schema_snapshots_equal(snapshot_deno, snapshot_rust, { a: 'deno', b: 'rus
295
305
  Each impl's _own_ tests still gate its DDL correctness independently — this
296
306
  pair is purely the cross-impl drift check.
297
307
 
308
+ ## Cross-impl differential
309
+
310
+ The behavioral twin of the two parity gates above: where they compare
311
+ declarations (DDL, the method set), this compares answers. Each impl is the
312
+ oracle for the other — a case the table pins by status alone, or an extra
313
+ field a loose output schema passes on one spine, shows up as a difference.
314
+
315
+ - `cross_backend/differential_normalize.ts` — `normalize_differential_response({status, headers, body})`
316
+ → `DifferentialResponse` `{status, body, header_names}`. Volatile values
317
+ become positional placeholders per kind (`VOLATILE_VALUE_PATTERNS`: API
318
+ tokens, `tok_` token ids, lowercase UUIDs, `ISO8601_SECONDS` timestamps,
319
+ bare 64-hex digests — matched inside any string, object keys included): the
320
+ first UUID is `<uuid:1>`, a repeat of it `<uuid:1>` again, so identity
321
+ relations within one response survive while the values differ across
322
+ backends. Placeholders are positional, not tied to fixture identity, so two
323
+ ids that appear once each and trade places normalize equal. Only the
324
+ canonical spelling is placeholdered — an uppercase UUID or a millisecond
325
+ timestamp stays raw and differs — and a `blake3:` content address is
326
+ compared verbatim (equal bytes hash equal on both spines). Keys are sorted (and visited
327
+ in that order when numbering); array order is kept. `header_names` is the
328
+ sorted name set minus `VOLATILE_HEADER_NAMES` (framing and per-request
329
+ headers each server sets its own way); values are never compared —
330
+ `set-cookie` carries a fresh credential, and the contract header values are
331
+ the conformance table's `expect.headers` + no-fingerprint floor.
332
+ `diff_differential_responses(a, b)` → `DifferentialDiff[]`, one per leaf
333
+ path (`status`, `headers.<name>`, `body.error.data.reason`; an object one
334
+ side lacks diffs key by key, and an empty one against a missing one at its
335
+ own path), `differential_path_covers(covered, path)`,
336
+ `format_differential_diffs(diffs, labels)`.
337
+ - `cross_backend/differential.ts` —
338
+ `describe_differential_conformance_tests({cases, backends: [a, b],
339
+ surface_source, rpc_endpoints, live_rpc_endpoints?, session_options,
340
+ principals?, known_divergences?, suite_name?})`, each backend `{name,
341
+ setup_test, ws?}`; methods resolve as the table resolves them
342
+ (`resolve_conformance_rpc_endpoints`).
343
+ `ws` on both runs the cases over their WebSocket endpoints, filtered by
344
+ `is_ws_conformance_case`, with the ledger checked against what is left; on
345
+ one alone, registration throws. Per case, each backend's own `setup_test`
346
+ (separate databases and keepers), the principal resolved and the request
347
+ sent through `send_conformance_case` (`conformance_table.ts`), both answers
348
+ normalized and diffed. The per-case `expect` is not asserted —
349
+ each backend's conformance leg holds it. `KnownDivergence` is the ledger
350
+ row: `{case_name | method, tracking_id, reason, what_differs}`, the names a
351
+ string or a list, `what_differs` the diff paths covered (a path covers
352
+ everything under it), each a string or a `KnownDivergencePath`
353
+ `{path, a?, b?}` pinning the value a side shows there (`undefined` for
354
+ nothing). A pin is compared with the normalized leaf value at the differing
355
+ path, so a placeholdered value never matches the raw one, an object pin
356
+ must list its keys sorted, and a parent-path pin never matches. A case may
357
+ differ at the paths of the entries it matches, with their values, and
358
+ nowhere else. `xfail_until`'s turns-red semantics clean
359
+ the ledger: a case a `case_name` entry names must still show it (checked in
360
+ that case's test), a `method` entry must still show on one of its cases
361
+ (checked by a closing test once they all ran — a filtered or failed run
362
+ leaves it unjudged, and the test skips when nothing was judged), and an
363
+ entry matching no case, or listing a case name that doesn't exist or a
364
+ method no case sends, fails —
365
+ the static half is `check_known_divergence_ledger(cases, ledger)`, exported
366
+ for checking a ledger without a backend. No other ignore path exists.
367
+
368
+ fuz_app runs it under the `cross_backend_parity` project
369
+ (`differential.cross.test.ts`, its ledger `differential_known_divergences.ts`
370
+ in `src/test/cross_backend/`): every `conformance_*_cases.ts` array plus the
371
+ generated auth rows (over the live mount too, so the cell verbs, the actor
372
+ resolvers and the `_testing_*` backdoors are compared), TS spine vs
373
+ `testing_spine_stub`, both on Postgres so no engine difference reads as a body
374
+ difference. The same cases run again over
375
+ WS, against `differential_ws_known_divergences` (the HTTP ledger without its
376
+ header row). The multi-actor `conformance_acting_cases.ts` rows run with the
377
+ generated rows on a keeper seeded with `extra_actors`, over both transports.
378
+ The TS spine runs under `DEV` there, so its DEV-only diagnostics (Zod
379
+ `issues` on `invalid_params`, the `X-Fuz-Auth-Debug` header) are ledger rows.
380
+ The suite composition is `describe_spine_differential_suites`
381
+ (`src/test/cross_backend/spine_differential.ts`), which takes the two
382
+ backends and their ledgers. fuz_app also holds two TS runtimes to each other
383
+ with it: the `cross_backend_ts_differential` project
384
+ (`differential_ts.cross.test.ts`) dual-spawns the TS spine binary on Node and
385
+ on Bun, both on `memory://`, against an empty ledger — both run under `DEV`,
386
+ so the DEV-only rows show on both sides and differ nowhere. It needs no Rust
387
+ or Postgres and runs in the `cross-ts` CI job.
388
+
298
389
  ## Assertions, coverage, helpers
299
390
 
300
391
  ### `assertions.ts` — surface + error-schema assertions
@@ -432,7 +523,7 @@ Also exported: `describe_adversarial_auth(options)` (group 3 on its own) and
432
523
  `build_error_schema_lookup(specs, middleware_specs?)` (pre-built
433
524
  `Map<string, RouteErrorSchemas>` for per-response validation).
434
525
 
435
- ### `adversarial_input.ts` — schema-walk payload generation
526
+ ### `adversarial_input.ts` + `adversarial_input_cases.ts` — schema-walk payload generation
436
527
 
437
528
  `describe_adversarial_input({build, roles})` — fires input body / params /
438
529
  query validation failures at every route with correct-auth credentials so
@@ -440,11 +531,12 @@ validation middleware is actually exercised (not short-circuited by 401).
440
531
  All cases expect 400 with one of `ERROR_INVALID_REQUEST_BODY` /
441
532
  `_INVALID_JSON_BODY` / `_INVALID_ROUTE_PARAMS` / `_INVALID_QUERY_PARAMS`.
442
533
 
443
- Exported generators:
534
+ The generators live in the hono-free `adversarial_input_cases.ts`, so the
535
+ cross-process suites reuse them:
444
536
 
445
- - `generate_input_test_cases(input_schema)` — whole-body structural (non-object, extra key when `strictObject`), missing required fields, one wrong-type per field, null for required non-nullable, one format violation per constrained field, numeric/array/string boundary cases via JSON Schema introspection.
446
- - `generate_params_test_cases(params_schema)` — format violations only (unconstrained string params accept anything).
447
- - `generate_query_test_cases(query_schema)` — missing required + format violations.
537
+ - `generate_input_test_cases(input_schema)` → `InputTestCase[]` — whole-body structural (non-object, extra key when `strictObject`), missing required fields, one wrong-type per field, null for required non-nullable, one format violation per constrained field, numeric/array/string boundary cases via JSON Schema introspection.
538
+ - `generate_params_test_cases(params_schema)` → `ParamsTestCase[]` — format violations only (unconstrained string params accept anything).
539
+ - `generate_query_test_cases(query_schema)` → `QueryTestCase[]` — missing required + format violations.
448
540
 
449
541
  GET-with-input routes hit the RPC `?params=` query convention; invalid-JSON
450
542
  arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema failure)
@@ -509,9 +601,15 @@ the response against declared schemas. DB-backed via `create_test_app`.
509
601
  Per-route test (`test.each`) — one line per route in the vitest output.
510
602
 
511
603
  Options: `{setup_test, surface_source, skip_routes?, input_overrides?, success_fixtures?}`.
512
- `input_overrides` is a `Map<"METHOD /path", body>` — override generated
513
- bodies for routes whose input schema can't round-trip cleanly (e.g. fields
514
- that must reference DB state).
604
+ Principals and the 401 check are the RPC round-trip's (`pick_auth_principal`;
605
+ `assert_route_authenticated` over `is_unauthenticated_refusal`, reading the
606
+ flat `error`), and a non-`GET` route called as the authed
607
+ account runs on a fresh account — a generated logout would otherwise end the
608
+ shared session, leaving `/password` and every later authed route a vacuous
609
+ 401. `src/test/testing/round_trip.test.ts` pins both, the RPC test's twin.
610
+ `input_overrides` is a `Map<"METHOD /path", body>` — override generated bodies
611
+ for routes whose input schema can't round-trip cleanly (e.g. fields that must
612
+ reference DB state).
515
613
 
516
614
  SSE routes are skipped by Content-Type sniff; `describe_sse_route_tests`
517
615
  picks them up separately.
@@ -521,9 +619,62 @@ picks them up separately.
521
619
  DB-backed round-trip for RPC: one POST test for all methods, one GET test
522
620
  for `side_effects: false` methods. Successful responses validate against
523
621
  `action.spec.output`; error responses validate as well-formed JSON-RPC error
524
- envelopes. Options: `{setup_test, surface_source, session_options, rpc_endpoints, skip_methods?, input_overrides?, success_fixtures?}`.
525
- The admin RPC auth test picks a session-based identity (`authed` / `admin` /
526
- bootstrapped keeper) based on `method.auth`; keeper uses the daemon token.
622
+ envelopes. Three refusals fail instead of passing as an envelope: `method not
623
+ found` (the backend lacks a method the surface declares), a crash — HTTP
624
+ 500, the `internal_error` code (-32603), or a ≥500 whose body is no JSON-RPC
625
+ envelope — and a 401 to a non-public call (`assert_rpc_authenticated`): the
626
+ call carried a credential its auth admits, so a 401 means it stopped working.
627
+ A public method may still answer 401 (a wrong-password login), and so may a
628
+ handler naming its own reason (below). A ≥500 carrying a declared code
629
+ (`service_unavailable` 503, `timeout` 504, a consumer code mapped in
630
+ `JSONRPC_ERROR_CODE_TO_HTTP_STATUS`) is a server condition and passes like a
631
+ 4xx. The failure names the method, status, code and message; a non-JSON body
632
+ is reported as text. Options: `{setup_test, surface_source, session_options,
633
+ rpc_endpoints, skip_methods?, input_overrides?, success_fixtures?}`.
634
+ `pick_rpc_auth` picks the principal from `method.auth` through
635
+ `pick_auth_principal` (`integration_helpers.ts`, shared with the REST
636
+ round-trip; `AuthPrincipal`): none for a public method, the keeper's daemon
637
+ token when `credential_types` names it, the admin account's session for an
638
+ admin-gated method, the keeper's session for another role, else the authed
639
+ account's session. The 401 check is `is_unauthenticated_refusal`: a 401
640
+ reading `authentication_required` or naming no reason fails; a handler's own
641
+ 401 reason on an authenticated call (`invalid_credentials` for a wrong current
642
+ password) passes. So a handler that answers 401 to a valid credential must name
643
+ its reason — `data.reason` over RPC, the flat `error` over REST: a reason-less
644
+ 401 reads as unauthenticated and fails the round-trip. TS's
645
+ `jsonrpc_errors.unauthenticated(message)` sends no `data` unless given one, so
646
+ pass `{reason}` as its second argument.
647
+
648
+ A side-effecting method picked for the authed account runs on a fresh account
649
+ of its own (`round_trip_uses_fresh_account`, keyed on the spec's
650
+ `side_effects`): generated params omit optional fields, and an omitted target
651
+ on a self-service method is the caller — `account_delete` with no
652
+ `account_id` deletes it, `account_session_revoke_all` ends its session — which
653
+ on the shared account would turn every later account method into a 401. Keyed
654
+ on `side_effects` rather than a list of known destroyers, so a consumer's
655
+ self-targeting method is covered without registering it. The keeper, its
656
+ daemon token and the admin account stay shared (one keeper per fixture, and
657
+ admin methods with generated nil ids target no existing account); a
658
+ credential they lose still fails on the 401 check.
659
+ `src/test/testing/rpc_round_trip.test.ts` pins both against a fake backend
660
+ whose side-effecting method ends the caller's session.
661
+
662
+ `success_fixtures` (`RpcSuccessFixtures`, method → `(fixture) =>
663
+ Promise<RpcSuccessFixtureCase>`) drives a **success** response for a method
664
+ whose generated nil ids can only refuse: the factory builds the row, then
665
+ returns `{params, headers?}` — `headers` replaces the picked identity, for a
666
+ row only its owner may act on. The response must be `ok`, not an internal
667
+ failure, and parse against `output`. `cross_backend/rpc_success_fixtures.ts`
668
+ — `create_spine_rpc_success_fixtures({rpc_path?})` builds the map for the
669
+ standard spine methods that act on an existing row (`account_token_revoke`,
670
+ `account_session_revoke`, `invite_delete`, `role_grant_revoke`,
671
+ `role_grant_offer_accept` / `_decline` / `_retract`) from standard RPC
672
+ actions and the builtin `ROLE_ADMIN` only, each factory on fresh accounts
673
+ and calling as one of them, so any consumer mounting
674
+ `create_standard_rpc_actions` can pass it. The fixtures run last on the
675
+ describe's fixture, since they mutate.
676
+ fuz_app wires it through the standard bundle and in the in-process
677
+ `cross_backend/rpc_round_trip_spine.db.test.ts`.
527
678
 
528
679
  ### `sse_round_trip.ts` — `describe_sse_route_tests`
529
680
 
@@ -666,7 +817,12 @@ lifecycle (via `role_grant_offer_create` + `role_grant_offer_accept` +
666
817
  `role_grant_revoke` RPC flows — **not** REST, **not** direct
667
818
  `query_accept_offer`; see `auth/CLAUDE.md` for
668
819
  `role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`),
669
- session / token management, audit log reads (RPC), admin-to-admin
820
+ session / token management, audit log reads (RPC — every row read is
821
+ parsed by `assert_audit_event_metadata`, closed to undeclared keys when the
822
+ fixture asks, and the completeness test drives login, logout, the offer flow,
823
+ grant + revoke, token create, password change, and — each where the surface
824
+ mounts its actions — token revoke, session revoke, invite create + delete and
825
+ `app_settings_update`), admin-to-admin
670
826
  isolation, error coverage, response schema validation.
671
827
 
672
828
  The shared `role_grant_offer_and_accept` helper (`role_grant_helpers.ts`)
@@ -679,6 +835,12 @@ from `db_entities.ts`.
679
835
 
680
836
  Required options: `{setup_test, surface_source, session_options, rpc_endpoints: RpcEndpointsSuiteOption, roles: RoleSchemaResult, admin_prefix?}`.
681
837
 
838
+ fuz_app runs it cross-process through the standard bundle
839
+ and in-process as a consumer wires it
840
+ (`cross_backend/admin_integration_spine.db.test.ts`) — the in-process fixture's
841
+ `create_account` inserts rows directly, writing no `invite_create` or `signup`
842
+ audit row, so the suite expects neither from it.
843
+
682
844
  `rpc_endpoints` is `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` —
683
845
  the same `RpcEndpointsSuiteOption` union every DB-backed suite accepts
684
846
  (`integration`, `admin_integration`, `audit_completeness`, `rate_limiting`,
@@ -763,6 +925,12 @@ invokes the right per-test fixture. Consumers wiring the standard surface
763
925
  call once instead of seven times; forgetting a suite no longer silently
764
926
  loses coverage.
765
927
 
928
+ `rpc_success_fixtures`, `rpc_skip_methods` and `rpc_input_overrides`
929
+ forward to the RPC round-trip's `success_fixtures`, `skip_methods` and
930
+ `input_overrides` (same options on `describe_standard_cross_process_tests`)
931
+ — `rpc_skip_methods` is the consumer escape hatch for a method the generic
932
+ round-trip can't drive in its test wiring.
933
+
766
934
  `StandardTestOptions` requires `create_route_specs` (for rate_limiting) and
767
935
  `rpc_endpoints` (for admin/audit_completeness/rpc_round_trip); the admin
768
936
  suite's requirement is enforced at the type level so a missing
@@ -810,6 +978,7 @@ One-shot transport:
810
978
  - `RpcCallResult` — discriminated `{ok: true, status, headers, result}` / `{ok: false, status, headers, error: {code, message, data?}}`. `headers` is the lowercased-key response-header snapshot so callers can assert header-level properties (e.g. the conformance runner's no-fingerprint invariant).
811
979
  - `headers_to_record(headers)` — snapshot a `Headers` into a lowercased-key `Record<string, string>` (multi-value collapses to the comma-joined form; `Set-Cookie` excluded). Used by `rpc_call` and the conformance table's REST branch to thread response headers through for assertions.
812
980
  - `RpcCallArgs` — `{app, path, method, params?, headers?, id?, verb?}`. `verb` defaults to `'POST'`; use `'GET'` for `side_effects: false` methods.
981
+ - `rpc_call_raw(args)` — the same request as `rpc_call`, the response unparsed: `RpcCallRawResult` `{status, headers, body}` with the whole envelope. `parse_rpc_call_result(raw, method)` turns it into an `RpcCallResult`; `rpc_call` is the two composed.
813
982
  - `rpc_call(args)` — merges `RPC_CALL_DEFAULT_HEADERS` (`host: 'localhost'`, `origin: 'http://localhost:5173'`, `Content-Type: 'application/json'`) under caller headers. Envelope-shape violations throw; JSON-RPC errors return `{ok: false, error}` so callers assert on `error.code` / `error.data.reason`.
814
983
  - `rpc_call_typed<T>(args, output_schema)` — parses the success `result` through the schema; throws on envelope failure, error response, or schema mismatch. Use `rpc_call` when the test needs to assert on error shapes.
815
984
  - `rpc_call_for_spec<TSpec>(args)` — spec-bound variant: takes `{..., spec, params}` in place of `{..., method, params}`. `params` is typed from `spec.input` and the success `result` is typed from `spec.output` (runtime-validated, same contract as `rpc_call_typed`). Error branch stays untyped (JSON-RPC `error.data` shapes vary per call site). Use at happy-path + denial-path call sites; fall back to `rpc_call` for adversarial tests that send deliberately-malformed params.
@@ -822,7 +991,7 @@ Registry lookups:
822
991
  - `RpcEndpointsSuiteOption` — union `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` accepted by every DB-backed suite's `rpc_endpoints` field.
823
992
  - `resolve_rpc_endpoints_for_setup(rpc_endpoints, session_options)` — resolves the union to an array for setup-time inspection (path lookup, `find_rpc_action` presence checks). Factory form is invoked once with a stub `AppServerContext`; the produced actions are discarded because `create_app_server` invokes the factory a second time per-test with its real ctx. Safe when the factory is pure wrt endpoint `path` and action `spec.method` list.
824
993
 
825
- ### `rpc_attack_surface.ts` — `describe_rpc_attack_surface_tests`
994
+ ### `rpc_attack_surface.ts` + `rpc_envelope_cases.ts` — `describe_rpc_attack_surface_tests`
826
995
 
827
996
  3 test groups for JSON-RPC endpoints:
828
997
 
@@ -833,8 +1002,8 @@ Registry lookups:
833
1002
  - **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md` §Keeper auth shape).
834
1003
  - correct auth passes (not 401/403)
835
1004
  - GET unauthenticated for `side_effects: false` reads
836
- 2. **RPC adversarial envelopes** — fixed set exercising dispatcher steps 1–2: non-JSON body, wrong `jsonrpc` version, missing `jsonrpc` / `method` / `id`, batch array, unknown method, GET missing `method`/`id`, GET invalid JSON params, GET non-object params, GET mutation method → `invalid_request`.
837
- 3. **RPC adversarial params** — reuses `generate_input_test_cases` but filters out structural cases (those hit envelope validation at step 1, not params validation at step 5). Every case expects 400 `invalid_params`.
1005
+ 2. **RPC adversarial envelopes** — the fixed set `create_rpc_envelope_cases(endpoint, {skip_methods?})` builds, exercising the envelope parse, method lookup, and GET read restriction: non-JSON body → `parse_error`, wrong `jsonrpc` version, missing `jsonrpc` / `method` / `id`, batch array, non-object POST `params` → `invalid_request`, unknown method → `method_not_found`, GET missing `method`/`id` → `invalid_request`, GET invalid-JSON params → `invalid_params`, GET non-object params on **every** read (each of `RPC_NON_OBJECT_PARAMS`: a number, an array, a string, a boolean) → `invalid_params`, GET mutation method → `invalid_request`, plus three ordering cases sent with no credential (an anonymous GET mutation and an anonymous batch, both `invalid_request`, never 401; an anonymous GET with non-object params on a read requiring an identity, the 401 — params validation runs after authorization). Each case is sent by the app its method's auth admits (`select_auth_app`) and pins status, code, echoed `id`, and `data.reason` (exact where the dispatcher sets one, absent otherwise) via `assert_rpc_envelope_refusal`. The cases are data (`RpcEnvelopeCase`) in the hono-free `rpc_envelope_cases.ts`, shared with the cross-process `describe_rpc_adversarial_cross_tests`; `to_rpc_envelope_request` builds a case's request, and `rpc_get_side_effects_reason(method)` is the GET read restriction's reason.
1006
+ 3. **RPC adversarial params** — `generate_rpc_params_test_cases(input)` (`rpc_envelope_cases.ts`): `generate_input_test_cases` minus its structural cases (a non-object `params` is refused at the envelope, not at params validation). Every case expects 400 `invalid_params`. Shared with the cross-process suite.
838
1007
 
839
1008
  Skips silently when `surface.rpc_endpoints` is empty. Uses stub deps — no
840
1009
  DB needed.
@@ -874,10 +1043,17 @@ test-server core below (fuz_app's own domain-free `testing_spine_server`, run
874
1043
  on Node + Deno + Bun). In-process is the fast feedback path; cross-process is the
875
1044
  source of truth for wire-shape conformance.
876
1045
 
1046
+ fuz_app's own spines run every single-backend suite below through one
1047
+ `describe_spine_cross_suites` call, `src/test/cross_backend/spine_suites.cross.test.ts`
1048
+ (§`cross_backend/spine_suites.ts`), so a suite's section names the suite, not a
1049
+ file; the dual-spawn and limiter-enabled suites name their own files.
1050
+
877
1051
  ### Fixture protocol + capabilities
878
1052
 
879
1053
  - `testing/cross_backend/setup.ts` — `SetupTest` / `TestFixture` /
880
- `TestAccountFixture` / `CreateTestAccountOptions` types,
1054
+ `CreateTestAccountOptions` types (`fixture.create_account` and
1055
+ `fixture.extra_accounts` hand out `TestAccount`s from `app_server.ts`, built
1056
+ by `to_test_account`),
881
1057
  `default_in_process_setup(options)` (wraps `create_test_app`; pass
882
1058
  `migration_namespaces` for suites needing tables beyond the auth-only
883
1059
  default — the cell parity suite passes `[CELL_MIGRATION_NS]`, and
@@ -913,7 +1089,11 @@ source of truth for wire-shape conformance.
913
1089
  lands with an email set — the hook the username-or-email login parity suite
914
1090
  uses; the in-process path sets it directly on the inserted row. Must be a
915
1091
  valid, per-test-unique `Email` (the `LOWER(email)` partial-unique index
916
- rejects collisions). Caller-supplied `username`s pass through _as-is_ now that the DB
1092
+ rejects collisions). The signup and login it drives go to the setup's
1093
+ `signup_path` / `login_path` (default `SPINE_ACCOUNT_SIGNUP_PATH` /
1094
+ `SPINE_ACCOUNT_LOGIN_PATH`, from `spine_surface_constants.ts`, which also
1095
+ carries the other account routes, `SPINE_WS_PATH`, and the prefix).
1096
+ Caller-supplied `username`s pass through _as-is_ now that the DB
917
1097
  wipes between tests — hardcoded names like `'user_two'` work and the
918
1098
  earlier uniquification prefix is gone. Every `TestFixture` also exposes
919
1099
  `fresh_transport({origin?: string | null})` — cookie-jar-free probe;
@@ -930,22 +1110,31 @@ source of truth for wire-shape conformance.
930
1110
 
931
1111
  - `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
932
1112
  (the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
933
- `cell_gated_create` / `account_lifecycle` / `fact_serving` / `ready` /
1113
+ `cell_gated_create` / `account_lifecycle` / `fact_serving` /
1114
+ `fact_serving_admin_hash` / `ready` /
934
1115
  `account_status` / `peer_request` /
935
1116
  `ws_handshake_pipelining` / `ws_account_actions` /
936
1117
  `ws_role_gated` / `ws_refusal_body` — each has a `test_if`
937
1118
  reader (`ws_refusal_body` picks how the front-door table reads a WS
938
1119
  refusal instead); `peer_request` (server-initiated requests — the ActionPeer
939
- `peer/ping` round-trip) is `true` for both the Rust spine and the TS spine
940
- (`BackendWebsocketTransport.request_connection`); `ts_default_capabilities`
941
- keeps it `false` like sse/ready until a backend wires the `peer/ping` HTTP +
942
- WS mount), `test_if(cond, name, fn)`
943
- for capability-gated cases, and `in_process_capabilities` preset. The
944
- non-gating wiring facts (`bearer_auth` / `trusted_proxy` / `login_rate_limit`,
945
- no `test_if` readers) live in the parallel `BackendShapeNotes` record
946
- (`in_process_shape_notes`; `ts_default_shape_notes` / `rust_default_shape_notes`
947
- in `default_backend_configs.ts`) — split out so the capability type stops
948
- claiming gating it doesn't do. `cell_crud`
1120
+ `peer/ping` round-trip) is `true` on fuz_app's Rust and TS spine presets
1121
+ (`BackendWebsocketTransport.request_connection`); a backend states it
1122
+ `true` once it wires the `peer/ping` HTTP + WS mount), `test_if(cond, name, fn)`
1123
+ for capability-gated cases, and `in_process_capabilities` preset. Only
1124
+ flags a suite reads are capabilities — a wiring fact that gates nothing is
1125
+ not recorded. **Mount flags have no family default**: `MountFlag` names the
1126
+ flags that describe a backend's own mounts (`ws`, `sse`, `cell_crud`,
1127
+ `cell_relations`, `cell_gated_create`, `fact_serving`,
1128
+ `fact_serving_admin_hash`, `ready`,
1129
+ `peer_request`, `ws_account_actions`, `ws_role_gated`), and the family
1130
+ defaults (`ts_default_capabilities` / `rust_default_capabilities`, typed
1131
+ `FamilyDefaultCapabilities`) omit them, with `capabilities` required on
1132
+ `make_default_{ts,rust}_backend_config` — so a config that leaves one out
1133
+ fails typecheck naming it, rather than silently skipping its suites. The
1134
+ family defaults carry the declared surface's mounts (`account_lifecycle`,
1135
+ `account_status`) and the runtime-behavior flags (`ws_handshake_pipelining`,
1136
+ `ws_refusal_body`) of the family's reference runtime — Node for TS, hyper
1137
+ for Rust; a backend on another runtime (Deno, Bun) overrides them. `cell_crud`
949
1138
  gates the CRUD parity suite, `cell_relations` the relation / ACL / audit
950
1139
  parity suite — both `true` on every backend that live-mounts the full cell
951
1140
  surface (TS spine binary, in-process app, Rust stub). A backend mounting only
@@ -965,13 +1154,19 @@ source of truth for wire-shape conformance.
965
1154
  auto-enumerated for shape/auth) — the flag gates the behavioral parity the
966
1155
  generic round-trip can't drive (it can't delete the subject). `true` on every
967
1156
  spine. `fact_serving`
968
- gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference +
969
- admin-only bare-hash fact-serving parity suite, incl. the served fact's
970
- hardening headers and bodyless masked 404s); like cells it stays off the
971
- declared surface and is `true` on every spine that mounts the serve routes +
1157
+ gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference
1158
+ fact-serving parity suite, incl. the served fact's hardening headers and
1159
+ bodyless masked 404s); like cells it stays off the declared surface and is
1160
+ `true` on every spine that mounts the cell-gated serve route +
972
1161
  the `_testing_put_fact` seeder (whose optional `holder` records a
973
- `fact_holder` row, so a non-admin can reference what it seeds) — it also
974
- gates `describe_cell_ref_admission_cross_tests`. `ready` gates `describe_ready_cross_tests`
1162
+ `fact_holder` row, so a non-admin can reference what it seeds, and whose
1163
+ optional `external_url` writes a bare external row naming that URL, so the
1164
+ suite can pin that a URL naming another hash's file is refused) — it also
1165
+ gates `describe_cell_ref_admission_cross_tests`. `fact_serving_admin_hash`
1166
+ is the admin-only bare-hash route (`GET /api/facts/:hash`), its own flag
1167
+ because a backend can serve the cell-gated route alone: it gates the
1168
+ fact-serving suite's bare-hash cases (with `fact_serving`) and the bare-hash
1169
+ cases of the query-shape and token-scope-surface suites. `ready` gates `describe_ready_cross_tests`
975
1170
  (anonymous `GET /ready` → `200 {ready: true}` on a clean spine bootstrap);
976
1171
  like cells/sse the `/ready` deploy gate stays off the declared surface, `true`
977
1172
  on every spine that live-mounts it over the shared `expected_schema.json`.
@@ -991,7 +1186,7 @@ source of truth for wire-shape conformance.
991
1186
  as it does behind auth, and `false` in-process, where there is no socket to
992
1187
  write raw bytes to).
993
1188
  `ws_account_actions` (the WS endpoint mounts the account actions — `true` on
994
- fuz_app's own spine presets, `false` in the family defaults) gates the WS
1189
+ fuz_app's own spine presets; a consumer states it like every mount flag) gates the WS
995
1190
  round-trip suite's self-revocation case (which requires the reply ahead of
996
1191
  the close on every backend) and, with `rpc_path`, its WS-mutation audit case
997
1192
  (which calls `_testing_drain_effects`).
@@ -1024,6 +1219,129 @@ consumer needs partial opt-out, add the knob then.
1024
1219
  `bootstrap`, `rate_limiting_app_options`, `bootstrap_token`) — those drive
1025
1220
  the omitted suites.
1026
1221
 
1222
+ ### `cross_backend/spine_suites.ts` — `describe_spine_cross_suites`
1223
+
1224
+ One call that runs every shipped single-backend cross suite against one
1225
+ spawned backend: `describe_spine_cross_suites(handle, {surface_source,
1226
+ session_options, rpc_endpoints, live_rpc_endpoints?, roles?, setup?, exclude?,
1227
+ overrides?, rpc_success_fixtures?, rpc_skip_methods?, rpc_input_overrides?,
1228
+ rpc_max_body_bytes?, ws_max_message_bytes?})`.
1229
+ `live_rpc_endpoints` is the backend's whole live mount; given, `rpc_adversarial`
1230
+ also walks the methods it keeps off the declared surface. Without it a
1231
+ consumer writes a file per suite, and a suite added here never reaches it.
1232
+
1233
+ - **The registry.** `SUITE_NAMES` / `SuiteName` — the closed set of suites the
1234
+ call runs, each named by its module (`ws_round_trip` is
1235
+ `describe_cross_process_ws_tests`, `standard` the standard bundle).
1236
+ `STANDALONE_CROSS_SUITES` (keyed by the closed `StandaloneSuiteName`) names
1237
+ the modules left out, each with why: the
1238
+ `differential` (two handles), `login_security` and `ws_action_rate_limit`
1239
+ (a backend spawned with live limiters, which the standard suites would
1240
+ exhaust), and the `conformance_table` runner (the consumer's own cases).
1241
+ `src/test/testing/cross_backend/spine_suites.test.ts` imports every module
1242
+ under `cross_backend/` and holds the two lists to the ones exporting a
1243
+ `describe_*` function, whatever the export form (the Bun and Deno adapters,
1244
+ which load only on their own runtime, are held to naming no `describe_` at
1245
+ all), so a new suite fails until it is registered or stated standalone; the
1246
+ dispatch table
1247
+ is a `Record<SuiteName, …>`, so a registered name without an entry fails
1248
+ typecheck.
1249
+ - **Options from the handle.** Paths, cookie name and capabilities come from
1250
+ `handle.config` — `to_cross_suite_options(handle)`, exported for a suite
1251
+ called on its own — the daemon token from the handle, and each suite's
1252
+ `setup_test` from `default_cross_process_setup` over the `setup` options
1253
+ plus the seeding the suite needs (concatenated): a keeper-only
1254
+ `non_admin_keeper` for the standard bundle's keeper ≠ admin probe, the
1255
+ multi-actor keepers of `fact_serving` and `transport_composition`, the
1256
+ offer grantor of `role_grant_offer_enumeration`, the `cell_editor` holder of
1257
+ `cell_grant_role`. Suites are registered without a wrapping `describe`, so
1258
+ test names match a standalone call.
1259
+ - **`ws_required_roles`** — the roles the backend's WS upgrade on `ws_path`
1260
+ requires (an admin-gated `/api/ws`: `[ROLE_ADMIN]`). The suites that open
1261
+ sockets for accounts they create (`credential_deadline`, `session_cap`,
1262
+ `ws_round_trip`) create them holding these (`ws_account_roles`), and
1263
+ `role_grant_offer_notification_ws` expects revoking one to close the socket
1264
+ (its `ws_required_roles`) — one option for every socket a role gates.
1265
+ - **`rpc_max_body_bytes`** / **`ws_max_message_bytes`** — the backend's
1266
+ request-body cap on `rpc_path` and its inbound WS message cap, when they
1267
+ aren't the spine defaults (`SPINE_BODY_LIMIT_DEFAULT_BYTES`,
1268
+ `DEFAULT_WS_MAX_MESSAGE_BYTES`). The first reaches `body_size`,
1269
+ `body_size_smuggling` and `transport_composition`'s RPC over-cap row (each
1270
+ takes it as `rpc_max_body_bytes`), the second `ws_round_trip`'s
1271
+ `max_message_bytes` — one value per limit for every suite probing it.
1272
+ - **`exclude`** — `{[suite]: reason}`; the reason is required and shows in the
1273
+ run as a skipped test. An empty reason, an unknown suite name, or a suite
1274
+ both excluded and overridden throws before anything registers.
1275
+ - **`overrides`** — `SuiteOverrides`, per suite a `Partial` of its own options
1276
+ (`SuiteOptionsMap`), shallow-merged over the derived ones: a role list, an
1277
+ `xfail`, a narrower capability set, `ws: undefined` to drop optional socket
1278
+ cases (`session_cap` and `testing_backdoor` take no `capabilities`, so `ws` is
1279
+ their only socket switch). An entry can instead be a function of the derived
1280
+ options returning the patch (`SuiteOverride`), for a change built on a
1281
+ derived value — an `xfail` added to the derived multi-actor setup — so the
1282
+ seeding isn't restated. After the merge, every option the suite's type
1283
+ requires (each entry's `required` set, exhaustive by type) must be neither
1284
+ `undefined` nor `''`, or the call throws at collection naming suite and key.
1285
+ - **Inputs the handle doesn't carry.** The declared surface is a required
1286
+ option. Paths and roles that name the reference spines' mounts (the
1287
+ role-gated WS endpoint and its roles) default to the `SPINE_*` constants —
1288
+ a suite whose flag is off never reads them, and a backend mounting its own
1289
+ overrides them. Anything else comes from the config when it can
1290
+ (`session_cookie_codec`'s `cookie_keys` from `SECRET_FUZ_COOKIE_KEYS`), else
1291
+ from `overrides`; with neither, the call throws naming the suite and the
1292
+ field — provide it or exclude the suite.
1293
+
1294
+ Each suite reads its own capability flags, so a backend's truthful
1295
+ `BackendConfig.capabilities` decides what runs. The role-shaped suites default
1296
+ to the spine's own roles — `cell_grant_role`'s `cell_editor`, the
1297
+ `participant` `role_grant_offer_lifecycle` offers and
1298
+ `role_grant_participation` confers — and take another through `suite_roles`
1299
+ (`SpineSuiteRoles`: `cell_grant`, `conferral`), which the call also seeds on
1300
+ the holder each needs (set it there, not in `overrides`, which would leave the
1301
+ seed on the default). A backend with only the builtin roles passes
1302
+ `{cell_grant: ROLE_KEEPER, conferral: ROLE_ADMIN}`: keeper reach is not admin
1303
+ reach, so every `cell_grant_role` arm still turns on the grant, while an admin
1304
+ views and edits every cell — `describe_cell_grant_role_cross_tests` refuses
1305
+ `ROLE_ADMIN` — and the scoped-assign case of `role_grant_participation` skips
1306
+ for a builtin. fuz_app's own wiring is
1307
+ `src/test/cross_backend/spine_suites.cross.test.ts`.
1308
+
1309
+ ### `cross_backend/rust_spine_consumer_surface.ts` — `create_rust_spine_consumer_surface_spec`
1310
+
1311
+ The declared surface of a consumer served by the Rust spine, for its cross
1312
+ suites: `create_rust_spine_consumer_surface_spec({session_options,
1313
+ domain_action_specs, extra_rpc_actions?, roles?, exclude_methods?,
1314
+ rpc_path?, host_gate?})`, built on `create_test_app_surface_spec` from the hono-free route
1315
+ shapes. It declares `/health`, the `ACCOUNT_ROUTER_PATHS` under
1316
+ `/api/account` (`/status`, `/login`, `/logout`, `/password` — what
1317
+ `fuz_auth::account_router` mounts, so the TS-only `/verify` stays off), signup,
1318
+ bootstrap as a `surface_only` shape, the daemon-token layer the consumer test
1319
+ binaries wire, a `host_validation` stub layer at the head of the middleware
1320
+ list when `host_gate` says the server turns on the spine's opt-in host gate
1321
+ (default off), and one RPC endpoint: the standard actions (over `roles`), the
1322
+ `extra_rpc_actions` (opt-in families built against the stub context), then the
1323
+ request-response, non-protocol `domain_action_specs` minus `exclude_methods`
1324
+ (methods the spec-derived suites can't drive; an entry naming no domain spec
1325
+ throws). `create_rust_spine_consumer_rpc_endpoints(options)` is that endpoint
1326
+ alone, for `describe_spine_cross_suites`'s `rpc_endpoints`. Handlers are
1327
+ no-ops — live dispatch happens in the spawned binary. A server mounting no
1328
+ signup router (a single-keeper daemon) is not this shape.
1329
+
1330
+ ### `cross_backend/provided_context.ts` — `inject_backend_handle`
1331
+
1332
+ Augments vitest's `ProvidedContext` with the keys the shipped global setups
1333
+ provide — `backend_handle` (`create_cross_backend_global_setup`) and
1334
+ `parity_handle_a` / `parity_handle_b` (`create_dual_spawn_global_setup`'s
1335
+ default keys) — and reads them: `inject_backend_handle()` /
1336
+ `inject_parity_handles()` return the handles rebuilt by
1337
+ `reconstruct_bootstrapped_handle`. The augmentation ships in the module's
1338
+ `.d.ts`, so a test file importing either helper gets the typed `inject` with
1339
+ no augmentation of its own — a type-only import of the module carries it too,
1340
+ and a global setup built on the shipped makers needs none. A project providing other
1341
+ keys — a dual spawn under its own `provide_keys`, a probe result — augments
1342
+ `ProvidedContext` for those itself; fuz_app's are in
1343
+ `src/test/cross_backend/cross_test_types.ts`.
1344
+
1027
1345
  ### `cross_backend/conformance_table.ts` + `conformance_case.ts` + `xfail.ts` — declarative behavioral/security cases
1028
1346
 
1029
1347
  The opinionated behavioral/security layer on top of the spec-derived
@@ -1034,7 +1352,7 @@ conformance cases assert _expected behavior_ — the security negatives
1034
1352
  wire-shape check passes green on even when behavior is wrong.
1035
1353
 
1036
1354
  - `conformance_case.ts` — `ConformanceCase` Zod schema:
1037
- `{name, request: {method, params?, as, verb?}, expect: {status,
1355
+ `{name, request: {method, params?, as, acting?, verb?}, expect: {status,
1038
1356
  error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case is **data** — `method`
1039
1357
  resolves its `input`/`output` from the live registry (RPC) or `RouteSpec`
1040
1358
  (the 6 REST auth routes), so the case never carries a schema. `as` is the
@@ -1067,10 +1385,17 @@ error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case i
1067
1385
  `expired_session` is the keeper behind an expired server-side session
1068
1386
  (`fixture.mint_expired_session()`: a backdated `auth_session` row behind a
1069
1387
  still-valid signed cookie, so the DB-row expiry gate is what refuses it).
1388
+ `acting` (`ConformanceActingBinding`) has the runner fill `params.acting`
1389
+ with a real actor id: `principal` (the actor of the account the
1390
+ principal's credential names), `extra_actor` (`fixture.extra_actors[0]`),
1391
+ `keeper_actor` (`fixture.actor`, foreign to every non-keeper principal);
1392
+ params must then be an object or omitted, without an `acting` of their own,
1393
+ and a principal presenting no account throws.
1070
1394
  `error_reason` is the imported
1071
1395
  `ERROR_*` constant (asserted against the RPC `error.data.reason` or the
1072
- REST flat-body `error`; the bare `unauthenticated()` 401 carries no
1073
- reason, so `status` pins that denial class). `equivalence_group` is the
1396
+ REST flat-body `error`; the dispatcher's 401 carries
1397
+ `authentication_required`, and a denial with no reason — a handler's bare
1398
+ `forbidden` — is pinned by `status`). `equivalence_group` is the
1074
1399
  **negative-space** axis: every case tagged with the same group string must
1075
1400
  produce a **byte-identical** normalized response (`{status, body}`),
1076
1401
  asserted per impl after the case loop. It promotes a masked pair
@@ -1089,14 +1414,76 @@ error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case i
1089
1414
  backend-fingerprinting oracle). Headers are deliberately kept out of the
1090
1415
  `equivalence_group` `{status, body}` comparison (`Set-Cookie` / `Date` vary).
1091
1416
  - `conformance_table.ts` — `describe_conformance_table_tests({cases,
1092
- setup_test, surface_source, capabilities, rpc_endpoints, session_options,
1093
- principals?, suite_name?})`. Same `{setup_test, surface_source,
1417
+ setup_test, surface_source, capabilities, rpc_endpoints, live_rpc_endpoints?,
1418
+ session_options, principals?, ws?, suite_name?})`. A case's RPC method
1419
+ resolves from `rpc_endpoints` first, then from `live_rpc_endpoints` — the
1420
+ endpoints the backend live-mounts, for methods kept off the declared surface
1421
+ (cell verbs, opt-in resolvers, `_testing_*` backdoors); a backend's whole
1422
+ live mount is a fine value, since a declared method still resolves from the
1423
+ declared surface. `resolve_conformance_rpc_endpoints({rpc_endpoints,
1424
+ live_rpc_endpoints?, session_options})` (exported) builds that lookup list,
1425
+ for the table and the differential alike. fuz_app's spine value is
1426
+ `create_spine_live_rpc_endpoints()` (`full_spine_mount.ts`), the binary's
1427
+ whole mount built on stub deps for resolution only. Same `{setup_test, surface_source,
1094
1428
  capabilities}` protocol every Tier 1 suite uses, so **one case array runs
1095
1429
  both transports** — in-process (`gro test`) and cross-process (the gate,
1096
- each backend's real auth resolution). `resolve_principal` maps the five
1430
+ each backend's real auth resolution). `resolve_conformance_principal` (exported) maps the five
1097
1431
  always-available principals to fixture accessors; `role_holder` /
1098
1432
  `wrong_role` read a seeded `extra_accounts` username named via
1099
- `options.principals`.
1433
+ `options.principals`. `send_conformance_case(c, {fixture, surface_source, principals?,
1434
+ resolved_rpc_endpoints})` (exported) is the send half — principal resolved,
1435
+ request sent, the raw `{target, status, headers, body}` back unasserted; the
1436
+ table asserts on it, and the cross-backend differential compares two.
1437
+ **WS leg:** `ws: WsEndpointTarget` (`{base_url, ws_path, origin?}`) on
1438
+ either sends each case as one frame on an admitted socket the principal
1439
+ opens — its headers on the upgrade, no `Origin` where the HTTP leg
1440
+ suppresses it, its own actor as `?acting=`. Only the TS spine resolves an
1441
+ actor at the upgrade, refusing a multi-actor account's upgrade without
1442
+ `?acting=` (400 `actor_required`); the Rust spine's upgrade resolves the
1443
+ account and ignores it. Per-frame dispatch resolves each frame's `acting` on
1444
+ both. The leg always sends `?acting=`, so it does not cover that
1445
+ upgrade-level difference; the front-door table's multi-actor row does. The frame is the envelope the HTTP
1446
+ body is (`perform_action_result_to_envelope` on TS), so the response reads
1447
+ `{status, headers: {}, body: frame}` with `status` the code's
1448
+ `jsonrpc_error_code_to_http_status` (200 for a result) and every row holds
1449
+ unchanged. What WS loses is headers — the TS DEV-only `X-Fuz-Auth-Debug`
1450
+ among them. `is_ws_conformance_case(c)` (exported) is the filter: RPC, sent
1451
+ `POST`, no header value pinned, a principal that can hold a socket — not
1452
+ `anonymous` / `bearer_browser` / `malformed_bearer` / `expired_session`,
1453
+ refused at the upgrade, nor `scoped_token`, refused `surface:ws_upgrade`
1454
+ there (a narrowed bearer can never send a frame, so no per-frame scope row
1455
+ exists; the front-door table pins the upgrade refusal). An equivalence
1456
+ group left with one member on the WS leg is dropped. Cross-process only.
1457
+ - `conformance_rpc_auth.ts` — `generate_rpc_auth_conformance_cases({surface_source,
1458
+ live_rpc_endpoints?, skip_methods?})` → `ConformanceCase[]` from each declared RPC method's
1459
+ `RouteAuth`, then each live-mounted one the declared surface lacks
1460
+ (`live_rpc_endpoints`, resolved — the rows then need the same endpoints on
1461
+ the runner's `live_rpc_endpoints`): `anonymous` → 401 `authentication_required` for every method
1462
+ needing an account or actor (all in the
1463
+ `RPC_AUTH_ANONYMOUS_EQUIVALENCE_GROUP`, since the 401 must not vary by
1464
+ method); for a `credential_types` gate, each builtin channel it refuses
1465
+ (`keeper` = session, `token` = api_token, `daemon` = daemon_token, each the
1466
+ keeper) → 403 `credential_type_required` with `required_credential_types`;
1467
+ for `roles` on a method admitting a session, `fresh_non_admin` → 403
1468
+ `insufficient_permissions` with `required_roles`; public methods get
1469
+ nothing. Params are `generate_valid_body` minus its generated `acting`
1470
+ (read before validation, so the generator's id would answer
1471
+ `actor_not_on_account`), so a missing gate reaches the handler rather than
1472
+ a 400. On an actor-bound method the credential and role rows bind
1473
+ `acting: 'principal'` instead — both spines resolve the acting actor before
1474
+ the credential gate, so a keeper with a second actor (`extra_actors`) would
1475
+ otherwise answer `actor_required` — and the rows hold for a single-actor and
1476
+ a multi-actor keeper alike. The rows pin each
1477
+ gate, not its order against validation (the security slate's
1478
+ 401-before-400 rows do). Skips `PROTOCOL_ACTION_METHODS` (fixed shared specs,
1479
+ WS-scoped — `cancel` has no reply, `peer/ping` is public — and covered by
1480
+ the WS suites). Given the live mount, the `_testing_*` backdoors get rows too:
1481
+ each is daemon-token-gated, so anonymous → 401 and the keeper's session and
1482
+ API token → 403 `credential_type_required`. Exists
1483
+ because the action-manifest parity gate compares declarations, not the
1484
+ live gates, and the Rust spine dispatches its own. Input-dependent handler
1485
+ checks stay hand-written rows. fuz_app spreads it into both conformance
1486
+ legs.
1100
1487
  - `xfail.ts` — `xfail_until(tracking_id, reason, name, fn)`, a thin
1101
1488
  `test.fails` wrapper for deferred-by-design rows (visible + self-cleaning:
1102
1489
  turns red when the gap closes, forcing marker removal). In-scope gaps fail
@@ -1106,7 +1493,36 @@ capabilities}` protocol every Tier 1 suite uses, so **one case array runs
1106
1493
  Wire from a `.db.test.ts` (in-process) and a `.cross.test.ts`
1107
1494
  (cross-process) with the same case array — fuz_app's own runner-proof is
1108
1495
  `../../test/cross_backend/conformance.{db,cross}.test.ts` sharing
1109
- `conformance_proof_cases.ts`.
1496
+ `conformance_proof_cases.ts`, the generated auth rows, and
1497
+ `conformance_admin_cases.ts` (offer list/history across accounts, invite
1498
+ errors, pagination bounds on the admin lists); two checks the table can't
1499
+ express ride beside it on both legs: the duplicate-invite refusal (two
1500
+ requests, `describe_invite_duplicate_tests` in `invite_duplicate.ts`) and the
1501
+ offer reads' cross-account refusal being identical for the keeper's real id
1502
+ and the nil id (`describe_offer_cross_account_tests` in
1503
+ `offer_cross_account.ts` — the existing id is known only to the fixture).
1504
+ The cross legs pass the spine's live mount, so the table also runs
1505
+ `conformance_actor_cases.ts` (`actor_lookup`'s empty-`ids` bound, the
1506
+ `actor_search` scope gate) and `conformance_cell_cases.ts` (the cell refusals
1507
+ one request reaches with no cell in place: `kind` inside `data`, `cell_get`
1508
+ naming neither `id` nor `path`, the relation lists naming neither or both
1509
+ endpoints — each `invalid_params` with its declared reason — a grant miss
1510
+ `cell_grant_not_found`, the `cell_list` pagination bounds and enum-valued
1511
+ filters, and the schema-first rows where a bad page or filter value must answer
1512
+ before a handler's reason; `conformance_cell_cases.test.ts` requires a `{}` row
1513
+ with a reason for every cell action whose input is marked
1514
+ `empty_input_handler_guarded`), with the
1515
+ generated rows over the off-surface methods; the in-process leg for those is
1516
+ `conformance_off_surface.db.test.ts`, over an app mounting the cell surface
1517
+ and the two resolvers. The cross leg runs the same cases
1518
+ over WS too, and both legs run `conformance_acting_cases.ts` with the
1519
+ generated rows on a keeper seeded with `extra_actors`: no `acting` or a
1520
+ malformed one → 400 `actor_required`; the nil id, an unused id and another
1521
+ account's real actor → one identical 400 `actor_not_on_account`; acting as
1522
+ the extra actor (no grants) → 403 `insufficient_permissions` on an admin
1523
+ method over a session and a bearer, and on the keeper-gated `account_purge`
1524
+ over the daemon token; acting as the bootstrap actor passes the same gates.
1525
+ The role gate reads the resolved actor's grants on both spines.
1110
1526
 
1111
1527
  ### `cross_backend/ws_round_trip.ts` — `describe_cross_process_ws_tests`
1112
1528
 
@@ -1117,7 +1533,7 @@ needs raw `base_url` / `ws_path` the standard bundle doesn't carry, mirroring
1117
1533
  how `describe_ws_round_trip_tests` sits beside `describe_standard_tests`
1118
1534
  in-process. `describe_cross_process_ws_tests({setup_test, capabilities,
1119
1535
  base_url, ws_path, origin?, rpc_path?, max_message_bytes?, logout_path?,
1120
- password_path?, max_tokens?})` opens a live
1536
+ password_path?, max_tokens?, ws_account_roles?})` opens a live
1121
1537
  `WebSocket` via `create_ws_transport` (the `ws` npm package) and asserts
1122
1538
  cases against the upgrade stack `register_ws_endpoint` wires (origin →
1123
1539
  `require_auth` → dispatch): authed upgrade round-trips `heartbeat`,
@@ -1147,7 +1563,9 @@ bearer socket (a socket on its other token stays open);
1147
1563
  bearer socket of the token the cap evicts. The account-wide rows come from
1148
1564
  `cross_backend/revocation_drivers.ts` (`create_account_revocations`, shared
1149
1565
  with the SSE suite), and a case that opens several sockets closes whichever
1150
- opened when a later open fails. Per-connection auth is enforced **at upgrade
1566
+ opened when a later open fails. The second accounts are created holding
1567
+ `ws_account_roles`, for a backend whose WS upgrade requires a role (an
1568
+ admin-gated `/api/ws` refuses a roleless account). Per-connection auth is enforced **at upgrade
1151
1569
  time**, so the negative upgrade cases assert the upgrade itself rejects, not
1152
1570
  a per-message error; the close cases prove a revocation closes an
1153
1571
  already-open socket, since per-message dispatch never re-checks credential
@@ -1166,7 +1584,7 @@ so it validates the transport without touching domain WS methods. Gated on
1166
1584
  `capabilities.ws`; cross-process only (needs a real bound socket — wire from
1167
1585
  a `*.cross.test.ts`, never an in-process setup). Authed cookies come from the
1168
1586
  fresh-per-test keeper via `fixture.transport.cookies()`, not the stale
1169
- globalSetup handle. fuz_app's own wiring is `src/test/cross_backend/ws.cross.test.ts`.
1587
+ globalSetup handle.
1170
1588
 
1171
1589
  ### `cross_backend/role_grant_offer_notification_ws.ts` — `describe_role_grant_offer_notification_ws_tests`
1172
1590
 
@@ -1183,7 +1601,23 @@ lifecycle RPC over `fixture.transport`, then
1183
1601
  strict-parses the delivered frame against its canonical params schema from
1184
1602
  `auth/role_grant_offer_notifications.ts` (the guard against serialization drift —
1185
1603
  field / null / datetime / the flat revoke shape / the supersede `reason` +
1186
- `cause_id`). Unlike `describe_cross_process_ws_tests` (consumer-agnostic,
1604
+ `cause_id`). Every case also opens a **bystander** — a third account's
1605
+ admitted socket, party to nothing the case drives — and once the targeted
1606
+ frame has landed sends a `heartbeat` on it and asserts it received nothing
1607
+ but the answers to its own requests. The barrier is what makes the silence
1608
+ mean something: both spines queue a fan-out to every recipient in one pass
1609
+ with no await in it, and neither writes a response ahead of a notification
1610
+ already queued for the same connection — the TS transport sends both through
1611
+ one outbound queue, and the Rust loop writes a connection's queued frames
1612
+ before a response (`flush_queued_frames`). So a frame the pass addressed to
1613
+ the bystander reaches it ahead of the barrier's response. That is strict on TS,
1614
+ which sends a fan-out in one synchronous closure. On Rust, `deliver_all` pushes
1615
+ to recipients one after another on one worker while the target's connection
1616
+ loop runs on another, so the target's frame can be written while the pass is
1617
+ still pushing — a window the barrier's round trip outlasts in practice. The
1618
+ check covers every pass of the drain: the Rust `PostCommitEffects::drain_all`
1619
+ joins effects whose notify futures push synchronously, so every pass
1620
+ completes in the drain's first poll. Unlike `describe_cross_process_ws_tests` (consumer-agnostic,
1187
1621
  `heartbeat`-only), this drives a real **domain** notification family — but one
1188
1622
  built from spine primitives only (accounts / role-grants / offers / WS), zero
1189
1623
  consumer domain, so it lives here and runs against any backend that wires the
@@ -1202,8 +1636,7 @@ grant and expects its socket to close with `WS_CLOSE_PERMISSION_REVOKED` (the
1202
1636
  notification may or may not land first, and is held to the flat shape when it
1203
1637
  does), where an ungated socket expects the frame. The offer/accept fixtures are `create_pending_role_grant_offer` /
1204
1638
  `create_active_role_grant` (`cross_backend/role_grant_cross_helpers.ts`).
1205
- Gated on `capabilities.ws`; cross-process only. fuz_app's own wiring is
1206
- `src/test/cross_backend/role_grant_offer_notification_ws.cross.test.ts`, and
1639
+ Gated on `capabilities.ws`; cross-process only. fuz_app's
1207
1640
  `role_grant_offer_notification_ws_gated.cross.test.ts` runs it again over the
1208
1641
  spine binaries' role-gated endpoint with `ws_required_roles` declared.
1209
1642
 
@@ -1225,7 +1658,6 @@ must hold `ROLE_ADMIN` and `scoped_role` (a role an admin can grant scoped).
1225
1658
  fuz_app's spine binaries mount `SPINE_ROLE_GATED_WS_PATH` gated on
1226
1659
  `SPINE_ROLE_GATED_WS_ROLES` (`admin` + `participant`). Gated on
1227
1660
  `capabilities.ws` and `capabilities.ws_role_gated`; cross-process only.
1228
- fuz_app's own wiring is `src/test/cross_backend/ws_role_gated.cross.test.ts`.
1229
1661
 
1230
1662
  ### `cross_backend/peer_ping_ws.ts` — `describe_peer_ping_ws_tests`
1231
1663
 
@@ -1249,8 +1681,7 @@ the HTTP no-transport path. Gated on `capabilities.peer_request` — `true` on
1249
1681
  both the Rust spine and the TS spine (the `BackendWebsocketTransport.request_connection`
1250
1682
  path + `register_action_ws` response correlation); `peer/ping` is a protocol
1251
1683
  action (manifest-excluded), so parity is behavioral here, not via the manifest
1252
- gate. Cross-process only; fuz_app's own wiring is
1253
- `src/test/cross_backend/peer_ping_ws.cross.test.ts`.
1684
+ gate. Cross-process only.
1254
1685
 
1255
1686
  ### `cross_backend/ws_inbound_dispatch.ts` — `describe_ws_inbound_dispatch_cross_tests`
1256
1687
 
@@ -1265,8 +1696,7 @@ client's responder withholds the server's ping echo until the test releases
1265
1696
  it — so the suite gates on `capabilities.peer_request`.
1266
1697
  `describe_ws_inbound_dispatch_cross_tests({setup_test, capabilities, base_url,
1267
1698
  ws_path})`. The wire literals are independent copies of the producers'
1268
- constants, so a value changed on one spine fails here. Cross-process only;
1269
- fuz_app's own wiring is `src/test/cross_backend/ws_inbound_dispatch.cross.test.ts`.
1699
+ constants, so a value changed on one spine fails here. Cross-process only.
1270
1700
 
1271
1701
  ### `cross_backend/ws_outbound_overflow.ts` — `describe_ws_outbound_overflow_cross_tests`
1272
1702
 
@@ -1295,8 +1725,7 @@ flood action is daemon-token-gated, so the flooding socket upgrades with the
1295
1725
  daemon token and no `Origin` (`create_ws_transport`'s `origin: null`). The wire
1296
1726
  literals are independent copies of the producers' constants. Gated on
1297
1727
  `capabilities.ws`; cross-process only, and only for a backend that mounts the
1298
- flood action on its WS endpoint. fuz_app's own wiring is
1299
- `src/test/cross_backend/ws_outbound_overflow.cross.test.ts`.
1728
+ flood action on its WS endpoint.
1300
1729
 
1301
1730
  ### `cross_backend/ws_connection_cap.ts` — `describe_ws_connection_cap_cross_tests`
1302
1731
 
@@ -1324,8 +1753,7 @@ overridden; pass the consumer's value when it sets one, `null` (cap disabled)
1324
1753
  to skip. **Consumer-agnostic** like the WS round-trip suite (`heartbeat` only).
1325
1754
  A separate call rather than more cases on that suite because each case opens
1326
1755
  more than a cap's worth of real sockets. Gated on `capabilities.ws`;
1327
- cross-process only. fuz_app's own wiring is
1328
- `src/test/cross_backend/ws_connection_cap.cross.test.ts`.
1756
+ cross-process only.
1329
1757
 
1330
1758
  ### `cross_backend/session_cap.ts` — `describe_session_cap_cross_tests`
1331
1759
 
@@ -1333,12 +1761,13 @@ Parity for the per-account session cap over real HTTP: logging in past
1333
1761
  `max_sessions` (default `DEFAULT_MAX_SESSIONS`) never refuses a login and
1334
1762
  evicts the oldest session.
1335
1763
  `describe_session_cap_cross_tests({setup_test, max_sessions?, login_path?,
1336
- status_path?, ws?})` — pass `ws` (`{base_url, ws_path, origin?}`) to also
1764
+ status_path?, ws?, ws_account_roles?})` — pass `ws` (`{base_url, ws_path, origin?}`) to also
1337
1765
  assert that the evicted session's socket is closed with
1338
1766
  `WS_CLOSE_SESSION_REVOKED` once the evicting login commits; omitted, that case
1339
- is skipped, so a caller passes it only for a backend with `capabilities.ws`.
1340
- Cross-process only. fuz_app's own wiring is
1341
- `src/test/cross_backend/session_cap.cross.test.ts`.
1767
+ is skipped, so a caller passes it only for a backend with `capabilities.ws`;
1768
+ `ws_account_roles` gives that case's account roles, for a backend whose WS
1769
+ upgrade requires one.
1770
+ Cross-process only.
1342
1771
 
1343
1772
  ### `cross_backend/sse_round_trip.ts` — `describe_cross_process_sse_tests`
1344
1773
 
@@ -1368,8 +1797,7 @@ admin each drop its stream — the rows `create_account_revocations`
1368
1797
  session closes the oldest and leaves the rest open. The data-frame + close
1369
1798
  cases gate on `rpc_path` (they drive the standard account/admin actions); all
1370
1799
  cases gate on `capabilities.sse`. Cross-process only — wire from a
1371
- `*.cross.test.ts`. fuz_app's own wiring is
1372
- `src/test/cross_backend/sse.cross.test.ts`, which runs on every backend — the
1800
+ `*.cross.test.ts`. It runs on every fuz_app backend — the
1373
1801
  TS spines wire `audit_log_sse` and the Rust `spine_stub` serves the same
1374
1802
  stream.
1375
1803
 
@@ -1387,7 +1815,7 @@ covered on WS and SSE at once. The drivers stand alone too: `cross_post_as`
1387
1815
  `cross_admin_token_revoke_all`, and `cross_account_purge` (over the daemon
1388
1816
  token, no `Origin`).
1389
1817
 
1390
- ### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` — cell parity suites
1818
+ ### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` + `cell_moderate.ts` — cell parity suites
1391
1819
 
1392
1820
  The cell-layer parity coverage is split across two sibling suites. Cells
1393
1821
  can't ride the generic `describe_rpc_round_trip_tests` (stateful verbs need a
@@ -1398,7 +1826,19 @@ cell validators (`describe_standard_cross_process_tests`' generic round-trip
1398
1826
  never sees them). Both parse every success response against the verb's Zod
1399
1827
  **output** schema, so a TS↔Rust envelope drift fails the suite — not just a
1400
1828
  payload-field drift. Call-site primitives (`cross_rpc_call` / `error_reason` /
1401
- `expect_output`) live in `cross_backend/cell_cross_helpers.ts`; the shared
1829
+ `expect_output`) live in `cross_backend/cross_rpc.ts`, shared by every
1830
+ stateful cross suite (a reply that is no JSON object throws naming the method,
1831
+ the HTTP status and the body's head), and every
1832
+ suite that reads audit rows goes through `cross_backend/audit_cross_helpers.ts`
1833
+ — `list_audit_events(fixture, params, rpc_path?)` drains
1834
+ (`cross_drain_effects`, the `_testing_drain_effects` barrier) before it lists and
1835
+ parses, so no reader skips the barrier, and parses every row's metadata
1836
+ (`assert_audit_event_metadata`, closed to undeclared keys when the fixture's
1837
+ setup asked), so every reader holds the rows it reads to their event's
1838
+ schema. A suite pinning a row's values uses `assert_audit_metadata_fields(fixture,
1839
+ metadata, expected, absent?)` — each field compared on its own so a consumer's
1840
+ added key passes, the `absent` keys refused, and the whole object compared only
1841
+ when the fixture closed the world; the shared
1402
1842
  options shape is `RpcPathCapabilityGatedCrossSuiteOptions` from
1403
1843
  `cross_backend/setup.ts`. `cross_backend/wire_shapes.ts` carries
1404
1844
  `assert_iso8601_seconds` / `assert_iso8601_seconds_nullable` — the byte-level
@@ -1411,7 +1851,7 @@ an audit row, an admin account listing, and an offer's `created_at` /
1411
1851
  imperative cross suite aliases: `CrossSuiteOptions` (`setup_test` alone),
1412
1852
  `CapabilityGatedCrossSuiteOptions` (+ `capabilities` — `ready`),
1413
1853
  `RpcPathCrossSuiteOptions` (+ `rpc_path?` — origin, body-size (which adds `base_url`), actor
1414
- lookup/search, app settings, testing backdoor), and the intersection
1854
+ lookup, app settings, testing backdoor), and the intersection
1415
1855
  `RpcPathCapabilityGatedCrossSuiteOptions` (every `cell_*` suite, query-shape,
1416
1856
  token-scope surface, fact-serving, account lifecycle). A suite takes the
1417
1857
  capability-carrying variant only when it actually gates cases on a flag, so
@@ -1424,20 +1864,28 @@ the option type states whether the bundle is read.
1424
1864
  `path` write by non-admin → 403 on create + update; `cell_get` with no
1425
1865
  id/path → `invalid_params`; null-auth `cell_list` `created_by` →
1426
1866
  `invalid_params`; a `kind` at `CELL_KIND_LENGTH_MAX` stored whole, one
1427
- UTF-16 code unit past it → `invalid_params` on create and as a list filter).
1867
+ UTF-16 code unit past it → `invalid_params` on create and as a list filter;
1868
+ `cell_list` pages honoring `limit` + `offset`, a page holding visible rows
1869
+ only and filling to `limit` past a newer run of rows the caller can't see —
1870
+ two owners' private cells, read by a non-owner and by anon — and an admin's
1871
+ pages tiling every row).
1428
1872
  - **`describe_cell_relations_cross_tests`** (gates on
1429
1873
  `capabilities.cell_relations`) — the verbs beyond CRUD: grant lifecycle
1430
1874
  (actor-shaped editor grant enables edit, manage-tier `cell_grant_list`,
1431
1875
  revoke), the now-reachable `cell_visibility_manage_only` 403 (editor-grant
1432
1876
  holder can't flip visibility), field set / forward+reverse list / idempotent
1433
1877
  delete, item insert / ordered forward+reverse list / move / idempotent
1434
- delete, clone shallow (shares edges) vs deep (clones children), and
1878
+ delete, an insert or move onto a taken position → `cell_item_position_taken`
1879
+ with the rows unchanged, clone shallow (shares edges) vs deep (clones children), and
1435
1880
  manage-tier `cell_audit_list` (owner reads the timeline; a viewer-grant
1436
1881
  holder who can `cell_get` still gets the IDOR 404). Only **actor-shaped**
1437
1882
  grants are exercised — role-shaped principals need a closed role registry the
1438
1883
  Rust spine deliberately lacks.
1439
- - **`describe_cell_gated_create_cross_tests`** + **`describe_cell_moderate_cross_tests`**
1440
- (gate on `capabilities.cell_gated_create`, **cross-only**) — the parent-aware
1884
+ - **`describe_cell_gated_create_cross_tests`** (`cell_gated_create.ts`) +
1885
+ **`describe_cell_moderate_cross_tests`** (`cell_moderate.ts`)
1886
+ (gate on `capabilities.cell_gated_create`, **cross-only**; both build their
1887
+ spaces on `TEST_SPACE_POLICY`, exported beside the authorizer in
1888
+ `test_cell_gated_create_authorize.ts`) — the parent-aware
1441
1889
  cell-creation authorizer (`CellCreateAuthorize`, **directory model**) +
1442
1890
  the `cell_moderate` verb parity proofs. Both spines mount the same
1443
1891
  `test_cell_gated_create_authorize` policy (its admin bypass and `min_role`
@@ -1451,7 +1899,9 @@ moderation_required}`, with `moderation` set per the verdict (`pending`+private
1451
1899
  **visible** one **403**s; plain parentless creates stay open. The moderate
1452
1900
  suite then proves the `pending → approved | rejected` transition: an admin
1453
1901
  (root manager) approves → public, the **author** self-approve → 403
1454
- (anti-self-approval), a non-viewer → 404, reject → stays private. It also
1902
+ (anti-self-approval), a non-viewer → 404, reject → stays private, and a cell
1903
+ with no governing root → 400 `cell_not_a_contribution` for any viewer, the
1904
+ 404 mask first for a non-viewer. It also
1455
1905
  pins the moderation read gate (a pending or rejected post reaches no
1456
1906
  non-steward reader past its author and admins on `cell_get`, `cell_list`,
1457
1907
  `cell_item_list`, and fact serving, even flipped public or granted while
@@ -1467,8 +1917,7 @@ moderation_required}`, with `moderation` set per the verdict (`pending`+private
1467
1917
 
1468
1918
  `cell_crud` / `cell_relations` gate `true` on TS + Rust (cells run on both, no
1469
1919
  `.skip`); `cell_gated_create` is cross-only (the policy is a spine-binary
1470
- fixture). Cross-process wiring is `src/test/cross_backend/cell.cross.test.ts`
1471
- (all the cell suites); the in-process legs (plain `gro test`) for crud/relations
1920
+ fixture). The in-process legs (plain `gro test`) for crud/relations
1472
1921
  are `src/test/auth/cell_crud_parity.db.test.ts`
1473
1922
 
1474
1923
  - `cell_relations_parity.db.test.ts`, sharing the full-surface
@@ -1484,11 +1933,59 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1484
1933
  `rust_spine_stub_backend_config()` as a convenience preset for the non-domain
1485
1934
  third spine consumer, but otherwise backend-specific paths and env are a
1486
1935
  consumer concern.
1936
+ - `testing/cross_backend/default_backend_configs.ts` —
1937
+ `make_default_ts_backend_config` / `make_default_rust_backend_config`, the
1938
+ family builders a consumer config composes: the env baseline (`HOST` bound
1939
+ to `DEFAULT_TEST_BACKEND_HOST`, `127.0.0.1`; the port under
1940
+ `port_env_var`; cookie keys; allowed origins; the bootstrap token path; the
1941
+ Rust startup self-wipe), the default paths, and the required `capabilities`.
1942
+ `daemon_token_dir_env` names the env var the binary reads its daemon-token
1943
+ directory from, set to the `{dir}` whose `{dir}/run/daemon_token` is the
1944
+ bootstrap's `daemon_token_path` (`to_daemon_token_dir`, which throws for a
1945
+ path in another layout).
1946
+ - `testing/cross_backend/create_cross_backend_global_setup.ts` —
1947
+ `create_cross_backend_global_setup({configs, derive_name?, provide_key?,
1948
+ ensure_databases?, before_spawn?})`, the single-backend `globalSetup` maker:
1949
+ the backend name derived from the vitest project name selects a config,
1950
+ which is spawned, bootstrapped and provided. `before_spawn` (by backend
1951
+ name; a key naming no config, an inherited one included, throws at
1952
+ construction) runs first for the selected backend — `prepare_cargo_binary`
1953
+ for a Rust one — then its config factory, then `ensure_databases: true`
1954
+ creates the database the config's `postgres:` `env.DATABASE_URL` names
1955
+ (`ensure_database_for_url`), then the spawn. A second argument
1956
+ (`CrossBackendGlobalSetupDeps`) swaps the database creator and the spawn,
1957
+ for testing the setup itself.
1958
+ - `testing/cross_backend/cargo_binary.ts` — `prepare_cargo_binary({workspace_dir,
1959
+ package, bin?, profile?})` builds a Cargo binary (`cargo build -p --bin
1960
+ --profile`, output inherited, a failed build throws) unless
1961
+ `NO_REBUILD_ENV` (`FUZ_TESTING_NO_REBUILD`, `is_no_rebuild_requested`) asks
1962
+ to skip it, and returns its path; `resolve_cargo_binary_path(options)` is the
1963
+ path alone, for a config's `start_command` — under the `target_directory`
1964
+ `cargo metadata` reports (memoized per process per workspace), in the
1965
+ profile's directory (`to_cargo_profile_dir`: `dev` → `debug`;
1966
+ `to_cargo_binary_path` reads it from the metadata JSON). Run as
1967
+ `create_cross_backend_global_setup`'s `before_spawn` hook, the build happens
1968
+ before the spawn's startup window, with its output shown. fuz_app's own stub
1969
+ rebuild (`rebuild_rust_spine_stub`) is built on it.
1970
+ - `testing/cross_backend/postgres_database.ts` — `ensure_database(database,
1971
+ server_url?, deps?)` (idempotent `createdb` on the server the URL names, its
1972
+ password passed in the environment; `createdb`'s already-exists refusal is
1973
+ the quiet success case, `is_createdb_already_exists`, and any other failure
1974
+ is reported through `deps.warn` — `console.warn` by default — with
1975
+ `createdb`'s stderr, never thrown), `ensure_database_for_url(url, deps?)` (the
1976
+ database a `postgres:` URL names; anything else left alone),
1977
+ `with_database_name`, `to_maintenance_database_url`, `redact_database_url`
1978
+ (a URL fit to print), `is_postgres_url`. The harness creates a database only
1979
+ when a setup asks, so a test role needs no `CREATEDB` grant otherwise.
1487
1980
  - `testing/cross_backend/spawn_backend.ts` — `spawn_backend(config) => BackendHandle`.
1488
- Writes the bootstrap token, spawns `detached: true` in its own process
1489
- group (so SIGTERM to the negative pid tears down descendants), polls
1490
- health, reads the deterministic daemon token from the binary-written
1491
- file. Registers exit-time + signal cleanup so vitest worker death or
1981
+ Refuses a port that already accepts connections, writes the bootstrap
1982
+ token, deletes a stale daemon-token file, spawns `detached: true` in its
1983
+ own process group (so SIGTERM to the negative pid tears down descendants),
1984
+ polls health (each probe request timed out), reads the deterministic daemon
1985
+ token from the binary-written file. Keeps a bounded stderr tail
1986
+ (`create_output_tail`) for an early exit's error — with its exit code or
1987
+ signal — and for the log line an unexpected exit after startup writes.
1988
+ Registers exit-time + signal cleanup so vitest worker death or
1492
1989
  Ctrl+C kills children before they strand ports.
1493
1990
  - `testing/transports/fetch_transport.ts` — cookie-threading HTTP transport
1494
1991
  satisfying `RpcTestTransport`. Carries a name-keyed cookie jar that
@@ -1528,6 +2025,18 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1528
2025
  opens and round-trips `heartbeat` (response id `WS_ADMISSION_PROBE_ID`) so the
1529
2026
  connection is admitted when it resolves; suites that wait for a push, or need
1530
2027
  connections registered in a known order, open through it.
2028
+ - `testing/cross_backend/ws_endpoint.ts` — `WsEndpointTarget`
2029
+ (`{base_url, ws_path, origin?}`), the endpoint shape the socket-driving
2030
+ cross suites take (flat, or as an optional `ws` on
2031
+ `describe_session_cap_cross_tests`, `describe_testing_backdoor_cross_tests`
2032
+ and the conformance table; `describe_transport_composition_cross_tests`
2033
+ keeps its own nullable `base_url` and defaulted `ws_path`), and
2034
+ `open_session_ws(target, account, {admitted, on_request?, headers?,
2035
+ default_timeout_ms?})`, which opens a socket on an account's session cookie
2036
+ (`to_session_cookie`, asserted present) with `target.origin` or `base_url`
2037
+ as the `Origin` — through `create_admitted_ws_transport` when `admitted`,
2038
+ `create_ws_transport` otherwise. The cookie is the account's static one, so
2039
+ a session rotated through `fixture.transport` upgrades stale.
1531
2040
  - `testing/transports/ws_raw_client.ts` — `connect_raw_ws({port, hostname?, path, headers?, pipelined?})`
1532
2041
  → `RawWsClient` (`{frames, send_text, wait_for, destroy}`), a WebSocket client
1533
2042
  over a raw `node:net` socket for the two things a client library can't do:
@@ -1549,8 +2058,12 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1549
2058
  `raw_http_parse_response` (the first response, `null` until its head and
1550
2059
  framed body are whole), `raw_http_count_responses` (unanchored status-line
1551
2060
  count, so a response concatenated after a body is seen),
1552
- `raw_http_first_status`. Shared by the body-size, body-size smuggling, and
1553
- credential-header suites.
2061
+ `raw_http_first_status`, `raw_http_header_lines` (headers as CRLF lines),
2062
+ `raw_http_parse_json` (a body's JSON, `undefined` when it isn't), and
2063
+ `RAW_HTTP_READ_TIMEOUT_MS`, the suites' default read window. Shared by the
2064
+ body-size, body-size smuggling, credential-header, origin-scope and
2065
+ transport-composition suites, which take the 1 MiB cap they probe from
2066
+ `SPINE_BODY_LIMIT_DEFAULT_BYTES` (`spine_surface_constants.ts`).
1554
2067
  - `testing/transports/sse_frame_reader.ts` — `create_sse_frame_reader(reader, default_timeout_ms?)`,
1555
2068
  the transport-agnostic SSE framing core over a
1556
2069
  `ReadableStreamDefaultReader<Uint8Array>`: `\n\n` framing, per-read timeout,
@@ -1688,10 +2201,52 @@ refs); and a hidden cell's update, a create under a hidden parent, and a clone
1688
2201
  of a hidden source answer `404` whatever `data` names. Facts are put through
1689
2202
  `cross_put_fact` (exported by `fact_serving.ts`), whose `holder` maps onto
1690
2203
  `_testing_put_fact`'s optional `holder` key. Gated on
1691
- `capabilities.fact_serving`; cross-process only. fuz_app's own wiring is
1692
- `src/test/cross_backend/cell_ref_admission.cross.test.ts`; the in-process
2204
+ `capabilities.fact_serving`; cross-process only. The in-process
1693
2205
  cases are `src/test/auth/cell_ref_admission{,.fact_less}.db.test.ts`.
1694
2206
 
2207
+ ### Malformed-envelope and invalid-params parity — `cross_backend/rpc_adversarial.ts`
2208
+
2209
+ `describe_rpc_adversarial_cross_tests({setup_test, surface_source, rpc_path?,
2210
+ live_rpc_endpoints?, skip_methods?})` — the envelope and params groups of
2211
+ `describe_rpc_attack_surface_tests`, sent over `fixture.transport` to a
2212
+ spawned backend. The in-process groups run on stub apps, while the Rust spine
2213
+ parses envelopes and params by hand and the action-manifest parity gate
2214
+ compares no param schemas, so this is what holds the spines to one answer.
2215
+
2216
+ - **envelopes** — every `create_rpc_envelope_cases` case, one test each, on a
2217
+ fresh fixture; status, code, echoed `id`, and `data.reason` per case, and a
2218
+ guard test that the endpoint has both a read and an authenticated mutation to
2219
+ carry them.
2220
+ - **params** — one test per declared method, sending every
2221
+ `generate_rpc_params_test_cases` case and failing with the list of cases not
2222
+ answered 400 `invalid_params` with no `data.reason`. `xfail_params`
2223
+ (`RpcParamsXfail` — `{method, labels, tracking_id, reason}`) moves the named
2224
+ cases of a method into one `xfail_until` test each, for a known divergence on
2225
+ one backend; the method's other cases keep their test, and an entry naming no
2226
+ generated case fails. Pass it only for the backend that diverges — fuz_app
2227
+ keys it on `RUST_SPINE_STUB_DEFAULT_NAME`.
2228
+
2229
+ The authorized principal is the keeper on the first channel the method's auth
2230
+ admits — its session, else its daemon token or API token over a no-`Origin`
2231
+ transport — resolved through `resolve_conformance_principal`.
2232
+ **Preconditions:** the keeper holds every role a declared method gates on
2233
+ (`default_cross_process_setup` seeds `keeper` and `admin`; `extra_keeper_roles`
2234
+ for others) and has a single actor. No generated body carries a valid `acting`,
2235
+ and a malformed one reads as omitted ahead of validation on both spines, so a
2236
+ single-actor keeper resolves its actor and validation refuses; a keeper with a
2237
+ second actor answers `actor_required` first, so such a suite lists the
2238
+ actor-required methods in `skip_methods`.
2239
+
2240
+ **Off the declared surface.** `live_rpc_endpoints` (resolved) adds the methods
2241
+ the endpoint at `rpc_path` live-mounts beyond the declared surface — cell
2242
+ verbs, opt-in resolvers, `_testing_*` backdoors — to both groups: their
2243
+ envelope cases in a group of their own, and a params test per method,
2244
+ `_testing_*` included (a daemon-only backdoor goes with the keeper's daemon
2245
+ token). fuz_app's spine self-test passes `create_spine_live_rpc_endpoints()`.
2246
+ Skips `_testing_*` on the declared surface and `PROTOCOL_ACTION_METHODS`, which
2247
+ `generate_rpc_auth_conformance_cases` skips too.
2248
+ Ungated; cross-process only.
2249
+
1695
2250
  ### Token-scope surface parity — `cross_backend/token_scope_surface.ts`
1696
2251
 
1697
2252
  `describe_token_scope_surface_cross_tests({setup_test, capabilities, rpc_path?,
@@ -1706,7 +2261,7 @@ the spec-derived suites can't see it (REST routes, off the declared RPC surface)
1706
2261
  Mints a narrowed bearer through the production `account_token_create` path and
1707
2262
  fires it at the two surfaces whose denial **body** is observable over plain
1708
2263
  HTTP — the audit SSE stream (`capabilities.sse`) and the admin-only bare-hash
1709
- fact read (`capabilities.fact_serving`) — asserting the exact flat
2264
+ fact read (`capabilities.fact_serving_admin_hash`) — asserting the exact flat
1710
2265
  `{error: 'token_scope_required', required_scope: 'surface:<name>'}` both spines
1711
2266
  contract on (`token_scope_denied_body` ↔
1712
2267
  `fuz_auth::token_scope_surface_denied_response`).
@@ -1731,20 +2286,21 @@ _does_ name, and a full bearer must get past the scope gate on the same route.
1731
2286
  The WS upgrade is deliberately omitted — `create_ws_transport` threads cookies,
1732
2287
  not bearer headers, and a refused upgrade's body isn't readable, so it can't
1733
2288
  serve a body-parity gate (its per-spine coverage stays in the two surface
1734
- censuses). Cross-process only; fuz_app's own wiring is
1735
- `src/test/cross_backend/token_scope_surface.cross.test.ts`.
2289
+ censuses). Cross-process only.
1736
2290
 
1737
2291
  ### Front-door contract — `cross_backend/transport_composition.ts`
1738
2292
 
1739
2293
  `describe_transport_composition_cross_tests({setup_test, capabilities, base_url,
1740
- rpc_path?, origin?, rest_path?, password_path?, ws_path?, sse_path?})` — the per-transport
2294
+ rpc_path?, origin?, rest_path?, password_path?, ws_path?, sse_path?,
2295
+ multi_actor?, rpc_max_body_bytes?})` — the per-transport
1741
2296
  composition table: what each transport answers before per-action dispatch,
1742
2297
  when one request trips several refusals. Rows are REST `POST` (session-only
1743
2298
  `/api/account/logout`), RPC `POST`, RPC `GET`, the WS upgrade (gated on
1744
2299
  `capabilities.ws`) and the audit stream (`capabilities.sse`); columns are a
1745
2300
  foreign `Origin` with the keeper's session (→ 403 `forbidden_origin`), an
1746
2301
  over-cap declared length with a foreign `Origin` and no credential (→ 413
1747
- `payload_too_large`, body-carrying rows only), no credential (→ 401
2302
+ `payload_too_large`, body-carrying rows only; one byte past the spine default,
2303
+ or past `rpc_max_body_bytes` on the RPC row), no credential (→ 401
1748
2304
  `authentication_required`), and a narrowed bearer with no `Origin` (→ 403
1749
2305
  `token_scope_required` naming `rpc:<method>` on RPC and `surface:ws_upgrade` on
1750
2306
  the upgrade; 403 `credential_type_required` on the session-only REST route and
@@ -1754,12 +2310,17 @@ envelope's `error.data`. A WS refusal's body is required where the backend
1754
2310
  declares `capabilities.ws_refusal_body`; where it doesn't (Node's
1755
2311
  `@hono/node-ws` drops the body), an empty body passes on its status and a sent
1756
2312
  one is still asserted. The table
1757
- is the exported `transport_composition_cases` data. Given `base_url`, every
2313
+ is the exported `transport_composition_cases` data. `multi_actor:
2314
+ {setup_test, xfail?}` adds `transport_composition_multi_actor_cases`, run on
2315
+ a fixture whose keeper has `extra_actors`: the keeper's session upgrading with
2316
+ no `?acting=` → 400 `actor_required` (flat body), the TS upgrade's actor
2317
+ resolution. The Rust spine's upgrade resolves no actor and admits the socket,
2318
+ so fuz_app passes `xfail` (tracking id `rust-ws-upgrade-acting-fail-open`) for
2319
+ the Rust stub only. Given `base_url`, every
1758
2320
  probe is written to the socket (`raw_http_exchange`) and an over-cap request
1759
2321
  sends its head alone; with `base_url: null` the probes go through the
1760
2322
  fixture's transport and the WS rows skip. Not in the standard bundle. fuz_app's
1761
- own wiring is `src/test/cross_backend/transport_composition.cross.test.ts` and
1762
- the in-process `src/test/auth/transport_composition_parity.db.test.ts`.
2323
+ in-process leg is `src/test/auth/transport_composition_parity.db.test.ts`.
1763
2324
 
1764
2325
  `cross_backend/narrowed_api_token.ts` — `mint_narrowed_api_token({app,
1765
2326
  rpc_path, session_headers, methods, name?})` mints a method-scoped api token
@@ -1771,7 +2332,7 @@ principal.
1771
2332
  ### Credential deadline parity — `cross_backend/credential_deadline.ts`
1772
2333
 
1773
2334
  `describe_credential_deadline_cross_tests({setup_test, capabilities, base_url,
1774
- ws_path, sse_path?, rpc_path?})` — a connection closes when the credential it
2335
+ ws_path, sse_path?, rpc_path?, ws_account_roles?})` — a connection closes when the credential it
1775
2336
  opened on expires: each case pulls a fresh account's credential expiries in to
1776
2337
  two seconds with `_testing_shorten_credential_expiry` before the open, then
1777
2338
  checks the connection still dispatches short of the deadline and closes at it.
@@ -1781,17 +2342,24 @@ stream on a session (`capabilities.sse`) ends; and a second seam call asking
1781
2342
  for a later expiry moves nothing (the seam only shortens). API token × audit
1782
2343
  stream is not reachable over the wire — the stock stream is session-only — so
1783
2344
  the Rust spine pins it on a widened gate and the TS twin shares one deadline
1784
- wait across both transports. Cross-process only; fuz_app's own wiring is
1785
- `src/test/cross_backend/credential_deadline.cross.test.ts`.
2345
+ wait across both transports. The cases run concurrently (`describe.concurrent`)
2346
+ under one `setup_test()` for the suite, each on its own account, so a leg waits
2347
+ out one deadline rather than one per case. `ws_account_roles` gives the
2348
+ socket cases' accounts roles, for a backend whose WS upgrade requires one.
2349
+ Cross-process only.
1786
2350
 
1787
2351
  ### Origin check scope parity — `cross_backend/origin_scope.ts`
1788
2352
 
1789
- `describe_origin_scope_cross_tests({setup_test, capabilities, probe_path?})` —
1790
- the origin check covers paths outside `/api`: on `GET /ready` (`probe_path`), a
1791
- foreign `Origin` → 403 `forbidden_origin`, an absent or allowed one → 200. Gated
1792
- on `capabilities.ready`. Runs both legs (the in-process
2353
+ `describe_origin_scope_cross_tests({setup_test, capabilities, probe_path?,
2354
+ base_url?, unrouted_status?})` — the origin check covers paths outside `/api`:
2355
+ on `GET /ready` (`probe_path`), a foreign `Origin` → 403 `forbidden_origin`, an
2356
+ absent or allowed one → 200, gated on `capabilities.ready`. On an unrouted
2357
+ path, ungated: a foreign `Origin` → 403, an absent one passes to the fallback
2358
+ (`unrouted_status`, default 404 — a backend whose fallback serves an SPA shell
2359
+ passes 200; the in-process leg runs that shape too), and an over-cap `POST`
2360
+ with a foreign `Origin` → 413. Runs both legs (the in-process
1793
2361
  `cross_backend/origin_scope_parity.db.test.ts` + the cross-process
1794
- `origin_scope.cross.test.ts`).
2362
+ one call).
1795
2363
 
1796
2364
  ### Origin verification parity — `cross_backend/origin.ts`
1797
2365
 
@@ -1801,7 +2369,7 @@ imperative Origin-verification suite: absent `Origin` → request passes
1801
2369
  table's. Imperative (not a conformance-table row) because absent-Origin needs
1802
2370
  `fresh_transport({origin: null})`. Runs both legs (the
1803
2371
  in-process `auth/origin_parity.db.test.ts` + the cross-process
1804
- `origin.cross.test.ts`). The promotion surfaced a twin-impl divergence — the
2372
+ one call). The promotion surfaced a twin-impl divergence — the
1805
2373
  Rust spine returned a plain-text body — now converged to the canonical TS
1806
2374
  `{error: "forbidden_origin"}` via `fuz_http::forbidden_origin_response()`.
1807
2375
 
@@ -1821,7 +2389,6 @@ keeper's daemon-token channel; the TS unit leg runs the same rows through
1821
2389
  `run_testing_origin_check` (`src/test/http/origin.conformance.test.ts`), so the
1822
2390
  Rust spine is held to the table without reading the file. Ungated; edit the
1823
2391
  table by hand — it is the statement of the grammar, not generated output.
1824
- fuz_app's own wiring is `src/test/cross_backend/origin_conformance.cross.test.ts`.
1825
2392
 
1826
2393
  ### CORS simple-request parity — `cross_backend/simple_request.ts`
1827
2394
 
@@ -1837,7 +2404,7 @@ and login, form-encoded login, a forged `text/plain` logout with a session
1837
2404
  `application/json` with a charset is dispatched. The TS spine checks per route
1838
2405
  (`apply_route_specs`), the Rust spine in each JSON handler. Runs both legs:
1839
2406
  the in-process `auth/simple_request_parity.db.test.ts` and the cross-process
1840
- `simple_request.cross.test.ts`.
2407
+ one call.
1841
2408
 
1842
2409
  ### Query-string shape parity — `cross_backend/query_shape.ts`
1843
2410
 
@@ -1848,7 +2415,7 @@ suite over the three spine surfaces that read a query:
1848
2415
  unknown keys ignored, anonymous junk query → 401 never a query 400), the
1849
2416
  JSON-RPC GET endpoint (`method` / `id` / `params` first-wins — a last-wins
1850
2417
  reader dispatches a different method per backend on the same request), and
1851
- `GET /api/facts/:hash` (gated on `capabilities.fact_serving`; strict schema —
2418
+ `GET /api/facts/:hash` (gated on `capabilities.fact_serving_admin_hash`; strict schema —
1852
2419
  unknown key / malformed `acting` → 400 `invalid_query_params` **before** the
1853
2420
  401 guard, duplicated `acting` first-wins). Imperative because duplicated
1854
2421
  query keys can't be expressed through the structured RPC helpers. Pinned
@@ -1857,8 +2424,7 @@ after the Rust spine's derived-serde `Query` extractors were converged onto
1857
2424
  "duplicate field" 400 is an anonymously observable backend fingerprint).
1858
2425
  Cross-process only — the per-surface in-process TS pins live beside each
1859
2426
  module's own tests (`account_status.test.ts`, `action_rpc.test.ts`,
1860
- `serve_fact_route.db.test.ts`); fuz_app's own wiring is
1861
- `cross_backend/query_shape.cross.test.ts`.
2427
+ `serve_fact_route.db.test.ts`);
1862
2428
 
1863
2429
  ### Grant-verb role grammar parity — `cross_backend/role_grant_role_grammar.ts`
1864
2430
 
@@ -1873,8 +2439,7 @@ vacuous, and the same refusal for a role exactly at the length cap pins that
1873
2439
  boundary. Exists because the action-manifest parity gate captures no param
1874
2440
  schemas while the Rust spine parses params by hand, and the grant verbs write
1875
2441
  the role they refuse into their failure rows. Ungated; cross-process only (it
1876
- reads the audit log behind `_testing_drain_effects`). fuz_app's own wiring is
1877
- `src/test/cross_backend/role_grant_role_grammar.cross.test.ts`.
2442
+ reads the audit log behind `_testing_drain_effects`).
1878
2443
 
1879
2444
  ### API-token lifetime parity — `cross_backend/token_lifetime.ts`
1880
2445
 
@@ -1889,7 +2454,7 @@ schemas while the Rust spine hand-parses params — a spine that silently
1889
2454
  dropped the field would answer the same mint with a 200 and an eternal
1890
2455
  token; only the stored-state read-back catches it. Ungated (mint + list are
1891
2456
  on every spine's standard surface). Runs both legs
1892
- (`token_lifetime_parity.db.test.ts` + `token_lifetime.cross.test.ts`).
2457
+ (`token_lifetime_parity.db.test.ts` + the cross-process one call).
1893
2458
 
1894
2459
  ### Readiness probe parity — `cross_backend/ready.ts`
1895
2460
 
@@ -1904,7 +2469,7 @@ success-path gate. Gated on `capabilities.ready`. Imperative (not a
1904
2469
  JSON-RPC envelope, and the probe needs `fresh_transport({origin: null})` —
1905
2470
  the same reasons the sibling `cross_backend/origin.ts` suite is imperative.
1906
2471
  Runs both legs (`cross_backend/ready_parity.db.test.ts` +
1907
- `cross_backend/ready.cross.test.ts`).
2472
+ the cross-process one call).
1908
2473
 
1909
2474
  Both backends read the **same** committed
1910
2475
  `testing/cross_backend/expected_schema.json` (column-presence is engine-portable,
@@ -1917,6 +2482,20 @@ spine bootstrap (auth + cell + fact) and is regenerated +
1917
2482
  drift-guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`
1918
2483
  (`UPDATE_SCHEMA_READY=1`).
1919
2484
 
2485
+ ### Session cookie codec parity — `cross_backend/session_cookie_codec.ts`
2486
+
2487
+ `describe_session_cookie_codec_cross_tests({setup_test, cookie_name,
2488
+ cookie_keys, rpc_path?})` — both spines accept exactly one spelling of a signed
2489
+ session cookie. The suite holds the backend's `SECRET_FUZ_COOKIE_KEYS`
2490
+ (`cookie_keys`), signs `token:expires_at` itself, and probes `account_verify`
2491
+ with: a signature whose final character carries non-zero trailing bits (the
2492
+ same MAC under `atob`), an unpadded signature, and correctly signed
2493
+ `expires_at` values `+N`, ` N`, `N.0` and one past `u64::MAX` — each refused
2494
+ with `authentication_required`. Controls: re-signing the keeper cookie's
2495
+ payload reproduces it byte for byte and it authenticates, and a leading-zero
2496
+ `0N` authenticates (the `u64` bound is on the value, not the digit count).
2497
+ Ungated; cross-process only.
2498
+
1920
2499
  ### Testing-backdoor credential gate — `cross_backend/testing_backdoor.ts`
1921
2500
 
1922
2501
  `describe_testing_backdoor_cross_tests({setup_test, rpc_path?, ws?})` —
@@ -1937,11 +2516,14 @@ runs. Cited property: `docs/security.md`
1937
2516
  §Test Backdoor Actions. Cross-process only (the `_testing_*` actions are
1938
2517
  mounted on the spawned binary, not the in-process app — like the ws/sse
1939
2518
  suites); ungated, since every cross backend that uses
1940
- `default_cross_process_setup` mounts them. fuz_app's own wiring is
1941
- `src/test/cross_backend/testing_backdoor.cross.test.ts`. The complement is the
2519
+ `default_cross_process_setup` mounts them. The complement is the
1942
2520
  in-process spec-level gate test (`src/test/testing/testing_actions_auth.test.ts`,
1943
2521
  asserting each spec declares `credential_types: ['daemon_token']`) + the
1944
- `assert_no_testing_methods` surface invariant.
2522
+ `assert_no_testing_methods` surface invariant. fuz_app's conformance table
2523
+ also covers the same gates, through the auth rows it generates over the live
2524
+ mount (`live_rpc_endpoints`), which the differential compares across spines;
2525
+ this suite stays for consumers whose backends mount the `_testing_*` actions
2526
+ without a conformance table over their live mount.
1945
2527
 
1946
2528
  ### Identity-primitive parity — `cross_backend/identity_parity.ts`
1947
2529
 
@@ -1998,7 +2580,7 @@ USERNAME_LENGTH_MAX]` = `[3, 39]` boundary edges (just-outside → 400,
1998
2580
 
1999
2581
  Ungated (login + signup are on every spine). Runs both legs via the shared
2000
2582
  `{setup_test}` protocol: in-process (`identity_parity.db.test.ts`, plain
2001
- `gro test`) and cross-process (`identity_parity.cross.test.ts`).
2583
+ `gro test`) and cross-process (the one call).
2002
2584
  The accepted-case 403s rely on `create_spine_route_specs` zeroing
2003
2585
  `signup_fail_floor_ms` (mirroring `login_fail_floor_ms: 0`) so they don't each
2004
2586
  pay the 250ms denial floor.
@@ -2129,6 +2711,15 @@ need a real recipient): admin **assigns** `participant` to a fresh account
2129
2711
  `participant` → recipient **accepts** → a role_grant lands. The `role_holder`
2130
2712
  principal is seeded via `extra_accounts: [{username, roles: ['participant']}]`
2131
2713
  (bootstrap-cradle direct grant) and named through `principals.role_holder`.
2714
+ The replay cases count audit rows behind `_testing_drain_effects`, so the
2715
+ backend must mount it — every spine binary does; an in-process leg mounts
2716
+ `create_testing_drain_effects_action` over an emitter built with
2717
+ `track_inflight: true` (`conformance_participation.db.test.ts`).
2718
+ `role` (default `SPINE_PARTICIPANT_ROLE`) names the role conferred, which must
2719
+ be admin-grantable in the backend's registry; `scope_kind` (default `'space'`,
2720
+ or `null` for a builtin role) is the scope the scoped-assign case uses, and
2721
+ `null` skips it. A backend with only the builtin roles runs the suite with
2722
+ `ROLE_ADMIN` — the scoped case skips, as a builtin is global-only.
2132
2723
 
2133
2724
  Both legs run in-process (`conformance_participation.db.test.ts`, plain
2134
2725
  `gro test`) and cross-process (`conformance_participation.cross.test.ts`, TS
@@ -2137,6 +2728,38 @@ the `cross_backend_parity` action-manifest gate green (it was Rust-only before).
2137
2728
  Cited property: `docs/identity.md` + the role-gated-participation design
2138
2729
  (admin-only conferral, immediate assign).
2139
2730
 
2731
+ ### Offer decline + retract refusals — `cross_backend/role_grant_offer_lifecycle.ts`
2732
+
2733
+ `describe_role_grant_offer_lifecycle_cross_tests({setup_test, rpc_path?, role?})` —
2734
+ the error paths of the two verbs that end a pending offer without a grant,
2735
+ which the WS notification suite drives only on the happy path. Canonical
2736
+ answers, from the handlers and the queries behind them
2737
+ (`auth/role_grant_offer_actions.ts`, `auth/role_grant_offer_queries.ts`):
2738
+
2739
+ - **not a party → 404** `not_found` with reason `role_grant_offer_not_found`.
2740
+ Decline is scoped to the recipient account and retract to the grantor actor,
2741
+ so a stranger, the grantor declining, or the recipient retracting hears what
2742
+ a missing offer answers — the case compares the two refusals whole — and the
2743
+ offer stays pending (`docs/security.md` §"404-over-403 is the general mask").
2744
+ The scope check runs before the terminal check, so a stranger hears 404 on
2745
+ a decided offer too.
2746
+ - **decided → 400** `invalid_request` with reason `role_grant_offer_terminal`:
2747
+ a decline or retract after an accept, a second decline, a second retract,
2748
+ and either after the other.
2749
+ - **audit**: a decline writes one `role_grant_offer_decline` row
2750
+ (`account_id` / `actor_id` the recipient, `target_*` the grantor, metadata
2751
+ `{offer_id, role, scope_id}` plus `reason` only when one was given), a
2752
+ retract one `role_grant_offer_retract` row (the grantor, targeting the
2753
+ recipient, no target actor on an account-grain offer); refusals write none.
2754
+ Read through `list_audit_events`.
2755
+
2756
+ The single-request rows — decline and retract of an id naming no offer → 404
2757
+ — are in the conformance security slate, so the differential sends them to
2758
+ both spines. The keeper is the grantor and offers `role` — an admin-grantable
2759
+ role, `SPINE_PARTICIPANT_ROLE` by default; `ROLE_ADMIN` serves a backend with
2760
+ only the builtin roles. Ungated. The
2761
+ in-process leg is on the drained setup of `conformance_participation.db.test.ts`.
2762
+
2140
2763
  ### Building a TS test-server binary — `testing_server_core.ts` + adapters
2141
2764
 
2142
2765
  The reusable shape for standing up a **spawnable TS** cross-process test
@@ -2148,7 +2771,7 @@ re-roll the serve / daemon-info / WS-attach / drain boilerplate:
2148
2771
  - `testing/cross_backend/testing_server_deno.ts` — `create_deno_testing_adapter()` (`Deno.serve` + `@hono/deno`, an optional peer dep; `Deno` declared locally so it typechecks under the Node toolchain). Spawn the entry with `--sloppy-imports` (Deno doesn't do `.js`→`.ts`; Gro's loader does, so the Node path needs no flag).
2149
2772
  - `testing/cross_backend/testing_server_bun.ts` — `create_bun_testing_adapter()` (`Bun.serve` + `@hono/bun`'s module-level `getConnInfo`, `upgradeWebSocket`, and `websocket`; `Bun.serve` declared locally so it typechecks under the Node toolchain). Needs the optional `@hono/bun` peer dep (the counterpart to Node's `@hono/node-server` + `@hono/node-ws`; `Bun.serve` is built in), and Bun resolves `.js`→`.ts` natively (no flag, unlike Deno). Reuses `create_node_runtime` (Bun implements the `node:fs`/`node:process` surface). WS is module-level + stateless (like Deno) — the `websocket` handler is threaded into `serve`, where `Bun.serve` wants it, so no post-serve attach. It carries `maxPayloadLength` at twice `max_message_bytes` rather than Bun's 16 MiB default: past its cap Bun drops the socket without a close frame (1006), so the headroom leaves a message just over the limit to the per-message check (`WS_CLOSE_MESSAGE_TOO_BIG`, as on Node), while a grossly oversized one is refused before Bun buffers it whole.
2150
2773
  - `testing/cross_backend/default_spine_surface.ts` — the canonical no-domain spine surface (account/admin/audit/signup + bootstrap): `spine_session_options`, `spine_roles`, `create_spine_route_specs`, `spine_rpc_endpoints`, `create_spine_surface_spec`. This is the **declared** surface — the `create_standard_rpc_actions` bundle the spec-derived suites auto-enumerate. It passes `daemon_token: true` (both spine binaries mount the daemon-token layer) and leaves `GET /api/surface` at its default, off — the TS spine binary does too, and the Rust `testing_spine_stub` serves none. Shared by the spine_stub cross test, the TS cross tests, and the binary.
2151
- - `testing/cross_backend/full_spine_mount.ts` — `build_full_spine_rpc_actions(deps, options)` — the **full** live RPC mount: the declared bundle **plus** the off-declared-surface families the binary live-mounts (`_testing_*` backdoors, the cell verb set, the opt-in `actor_lookup` / `actor_search` resolvers). `build_full_spine_mount(deps, options)` builds it once for both of the binary's endpoints, returning `FullSpineMount` (`{rpc_actions, ws_actions}`): the WS list is `protocol_actions` plus every RPC action not already among them, twinning the Rust `testing_spine_stub`, which serves one registry on RPC and WS (the one difference is `cancel` — a registered protocol action on TS, owned by the read loop on Rust). Single-sources the mount so the binary and the `spine_method_coverage` reconciliation test build the same list.
2774
+ - `testing/cross_backend/full_spine_mount.ts` — `build_full_spine_rpc_actions(deps, options)` — the **full** live RPC mount: the declared bundle **plus** the off-declared-surface families the binary live-mounts (`_testing_*` backdoors, the cell verb set, the opt-in `actor_lookup` / `actor_search` resolvers). `build_full_spine_mount(deps, options)` builds it once for both of the binary's endpoints, returning `FullSpineMount` (`{rpc_actions, ws_actions}`): the WS list is `protocol_actions` plus every RPC action not already among them, twinning the Rust `testing_spine_stub`, which serves one registry on RPC and WS (the one difference is `cancel` — a registered protocol action on TS, owned by the read loop on Rust). Single-sources the mount so the binary and the `spine_method_coverage` reconciliation test build the same list. `create_spine_live_rpc_endpoints()` wraps the list, built on stub deps, as one endpoint at `SPINE_RPC_PATH` — the `live_rpc_endpoints` value the conformance table, the differential and the auth-row generator resolve off-surface methods from (no handler built there runs).
2152
2775
  - `testing/cross_backend/ts_spine_backend_config.ts` — `ts_spine_node_backend_config()` / `ts_spine_deno_backend_config()` / `ts_spine_bun_backend_config()` presets (in-memory pglet, no external infra), the TS analog of `rust_spine_stub_backend_config()`.
2153
2776
 
2154
2777
  fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` (`rate_limiters: 'disabled_for_testing'` plus whichever login / action limiters its env toggles built, `build_full_spine_mount`'s two lists on one `rpc_endpoints` entry and two `ws_endpoints` entries — `/api/ws` and the role-gated `SPINE_ROLE_GATED_WS_PATH` — sharing one `transport`, which is also the role-grant-offer `notification_sender`), reusing `default_spine_surface`. `src/test/cross_backend/testing_spine_server.db.test.ts` builds it with the Node adapter in-process and asserts its surface lists both WS endpoints (the gated one with its `required_roles`) and passes the surface invariants. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic. It leaves `create_app_server`'s `auth_cleanup` off, as the Rust `testing_spine_stub` leaves its twin unscheduled: a background pass would delete rows and write audit rows under a running suite. A consumer's cross-process test binary may schedule it when nothing its suites seed is already expired (the conformance table's `expired_session` principal seeds a backdated session row); an in-process harness leaves it off.