@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
@@ -1,7 +1,9 @@
1
1
  import './assert_dev_env.js';
2
2
  import { assert, beforeEach, afterEach } from 'vitest';
3
+ import { z } from 'zod';
3
4
  import { get_audit_metadata_validation_failures, get_audit_unknown_event_type_failures, reset_audit_metadata_validation_failures, reset_audit_unknown_event_type_failures } from '../auth/audit_log_queries.js';
4
5
  import { get_audit_write_failures, reset_audit_write_failures } from '../auth/audit_emitter.js';
6
+ import { audit_metadata_schemas } from '../auth/audit_log_schema.js';
5
7
  /**
6
8
  * Register per-test `beforeEach` + `afterEach` hooks that catch any audit
7
9
  * emission with a metadata shape that fails its `audit_metadata_schemas`
@@ -120,3 +122,51 @@ export const create_recording_audit_emitter = (calls_ref) => {
120
122
  };
121
123
  return { emitter, calls };
122
124
  };
125
+ /**
126
+ * The closed-world twin of a builtin event's `audit_metadata_schemas` entry:
127
+ * the same fields, with unknown keys refused. The registry's schemas are
128
+ * `z.looseObject` so a consumer can add fields to a builtin event (a builtin's
129
+ * schema can't be overridden), so this is opt-in.
130
+ */
131
+ const to_closed_metadata_schema = (schema) => {
132
+ if (schema instanceof z.ZodNullable) {
133
+ return to_closed_metadata_schema(schema.unwrap()).nullable();
134
+ }
135
+ if (schema instanceof z.ZodObject)
136
+ return z.strictObject(schema.shape);
137
+ return schema;
138
+ };
139
+ const declared_metadata_schemas = new Map(Object.entries(audit_metadata_schemas));
140
+ const closed_metadata_schemas = new Map(Object.entries(audit_metadata_schemas).map(([event_type, schema]) => [
141
+ event_type,
142
+ to_closed_metadata_schema(schema)
143
+ ]));
144
+ /**
145
+ * Assert an audit row read off the wire carries the metadata its event type
146
+ * declares — parsed against `audit_metadata_schemas[event_type]` as declared,
147
+ * so a mistyped, `null`, or missing field fails, and with
148
+ * `reject_unknown_keys` so does a key the event doesn't declare. The wire
149
+ * schema (`AuditLogEventJson.metadata`) is a loose record, and only the TS
150
+ * spine validates metadata, at insert and fail-open, so this is the check that
151
+ * holds a spine's rows to their event's contract.
152
+ *
153
+ * Builtin event types only: a consumer-registered type has no entry here, and
154
+ * passes unexamined.
155
+ *
156
+ * @param event - an audit row from `audit_log_list` or an audit read like it
157
+ * @param options - `reject_unknown_keys` for the closed-world check
158
+ * @throws AssertionError naming the event type and the schema issues
159
+ */
160
+ export const assert_audit_event_metadata = (event, options) => {
161
+ const schema = (options?.reject_unknown_keys ? closed_metadata_schemas : declared_metadata_schemas).get(event.event_type);
162
+ if (!schema)
163
+ return;
164
+ const parsed = schema.safeParse(event.metadata);
165
+ if (parsed.success)
166
+ return;
167
+ assert.fail(`audit row ${event.event_type} (outcome=${event.outcome}) carries metadata outside its ` +
168
+ `audit_metadata_schemas entry: ${JSON.stringify(event.metadata)} — ` +
169
+ parsed.error.issues
170
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
171
+ .join('; '));
172
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,uCAAuC,CAAC;AAEvF,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAqZF,CAAC"}
1
+ {"version":3,"file":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,uCAAuC,CAAC;AAEvF,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAwaF,CAAC"}
@@ -17,11 +17,12 @@ import '../assert_dev_env.js';
17
17
  * @module
18
18
  */
19
19
  import { describe, assert } from 'vitest';
20
- import { AccountDeleteOutput, AccountUndeleteOutput, AccountPurgeOutput, AdminAccountListOutput, AuditLogListOutput, ERROR_CANNOT_DELETE_KEEPER } from '../../auth/admin_action_specs.js';
20
+ import { AccountDeleteOutput, AccountUndeleteOutput, AccountPurgeOutput, AdminAccountListOutput, ERROR_CANNOT_DELETE_KEEPER } from '../../auth/admin_action_specs.js';
21
21
  import { ERROR_ACCOUNT_NOT_FOUND, ERROR_AUTHENTICATION_REQUIRED, ERROR_CREDENTIAL_TYPE_REQUIRED } from '../../http/error_schemas.js';
22
22
  import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
23
+ import { assert_audit_metadata_fields, list_audit_events } from './audit_cross_helpers.js';
23
24
  import { test_if } from './capabilities.js';
24
- import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
25
+ import { cross_rpc_call, error_reason, expect_output } from './cross_rpc.js';
25
26
  import { assert_iso8601_seconds } from './wire_shapes.js';
26
27
  import { SPINE_RPC_PATH } from './spine_surface_constants.js';
27
28
  export const describe_account_lifecycle_cross_tests = (options) => {
@@ -38,6 +39,15 @@ export const describe_account_lifecycle_cross_tests = (options) => {
38
39
  assert.strictEqual(deleted.deleted, true);
39
40
  const undeleted = expect_output(await cross_rpc_call(t, rpc_path, 'account_undelete', { account_id: victim.account.id }, admin_headers), AccountUndeleteOutput);
40
41
  assert.strictEqual(undeleted.undeleted, true);
42
+ // each verb audits the victim's identity snapshot, attributed to
43
+ // the admin (`list_audit_events` parses the metadata)
44
+ for (const event_type of ['account_delete', 'account_undelete']) {
45
+ const events = await list_audit_events(fixture, { event_type }, rpc_path);
46
+ const row = events.find((e) => e.outcome === 'success' && e.target_account_id === victim.account.id);
47
+ assert.ok(row, `${event_type} writes a success row targeting the victim`);
48
+ assert.strictEqual(row.account_id, fixture.account.id, event_type);
49
+ assert.strictEqual(row.metadata?.username, victim.account.username, `${event_type} snapshots the username`);
50
+ }
41
51
  });
42
52
  test_if(capabilities.account_lifecycle, 'purge (keeper, confirmed)', async () => {
43
53
  const fixture = await setup_test();
@@ -46,6 +56,11 @@ export const describe_account_lifecycle_cross_tests = (options) => {
46
56
  // Purge is keeper-gated: daemon-token credential, not a session.
47
57
  const purged = expect_output(await cross_rpc_call(t, rpc_path, 'account_purge', { account_id: victim.account.id, confirm: true }, fixture.create_daemon_token_headers()), AccountPurgeOutput);
48
58
  assert.strictEqual(purged.purged, true);
59
+ // the purge row outlives the account it names, carrying its identity
60
+ const events = await list_audit_events(fixture, { event_type: 'account_purge' }, rpc_path);
61
+ const row = events.find((e) => e.outcome === 'success' && e.target_account_id === victim.account.id);
62
+ assert.ok(row, 'account_purge writes a success row targeting the victim');
63
+ assert.strictEqual(row.metadata?.username, victim.account.username, 'account_purge snapshots the username');
49
64
  });
50
65
  test_if(capabilities.account_lifecycle, 'keeper guard: delete + purge refuse the keeper account', async () => {
51
66
  const fixture = await setup_test();
@@ -119,16 +134,11 @@ export const describe_account_lifecycle_cross_tests = (options) => {
119
134
  // and emits a forensic `outcome: failure` audit row.
120
135
  const del = await cross_rpc_call(t, rpc_path, 'account_delete', { account_id: fixture.account.id }, fixture.create_session_headers());
121
136
  assert.strictEqual(error_reason(del), ERROR_CANNOT_DELETE_KEEPER);
122
- // Deterministic barrier before reading: await in-flight
123
- // fire-and-forget audit writes on either spine.
124
- const td = fixture.fresh_transport({ origin: null });
125
- const drained = await cross_rpc_call(td, rpc_path, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
126
- assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
127
- // The failure row is now authoritative on both spines. `_testing_reset`
128
- // wiped audit_log at setup, so the refused delete is the only
129
- // account_delete event.
130
- const listed = expect_output(await cross_rpc_call(t, rpc_path, 'audit_log_list', { event_type: 'account_delete' }, fixture.create_session_headers()), AuditLogListOutput);
131
- const failure = listed.events.find((e) => e.outcome === 'failure' &&
137
+ // Read behind the drain barrier, so the failure row is authoritative
138
+ // on both spines. `_testing_reset` wiped audit_log at setup, so the
139
+ // refused delete is the only account_delete event.
140
+ const events = await list_audit_events(fixture, { event_type: 'account_delete' }, rpc_path);
141
+ const failure = events.find((e) => e.outcome === 'failure' &&
132
142
  e.metadata?.reason === ERROR_CANNOT_DELETE_KEEPER);
133
143
  assert.ok(failure, 'keeper-removal guard must emit an account_delete outcome=failure audit row with reason cannot_delete_keeper');
134
144
  // Byte-shape gate on the audit row's timestamp — `z.string()` in
@@ -151,6 +161,13 @@ export const describe_account_lifecycle_cross_tests = (options) => {
151
161
  const again = await cross_rpc_call(t, rpc_path, 'account_undelete', { account_id: victim.account.id }, admin_headers);
152
162
  assert.strictEqual(again.ok, false, 'double-undelete must not silently succeed');
153
163
  assert.strictEqual(error_reason(again), ERROR_ACCOUNT_NOT_FOUND);
164
+ // the refused undelete is audited as a failure naming the probed id
165
+ const events = await list_audit_events(fixture, { event_type: 'account_undelete', outcome: 'failure' }, rpc_path);
166
+ assert.strictEqual(events.length, 1, 'one account_undelete failure row');
167
+ assert_audit_metadata_fields(fixture, events[0].metadata, {
168
+ reason: ERROR_ACCOUNT_NOT_FOUND,
169
+ attempted_account_id: victim.account.id
170
+ });
154
171
  });
155
172
  // The last-admin guard (`ERROR_CANNOT_DELETE_LAST_ADMIN`) is **not**
156
173
  // cross-process-testable against this fixture: the per-test keeper
@@ -1 +1 @@
1
- {"version":3,"file":"actor_lookup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/actor_lookup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D;;;;;GAKG;AACH,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AASnE,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IAkDxF,CAAC"}
1
+ {"version":3,"file":"actor_lookup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/actor_lookup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D;;;;;GAKG;AACH,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AASnE,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IA8BxF,CAAC"}
@@ -6,22 +6,22 @@ import '../assert_dev_env.js';
6
6
  * (`{ids} → {actors: [{id, username, display_name?}]}`), not folded into the
7
7
  * standard bundle. It's live-mounted on the spine RPC path but kept off the
8
8
  * declared surface (`create_spine_surface_spec`) — like cells / ws / sse — so
9
- * the standard cross suite's generic round-trip never drives it; this
10
- * dedicated suite is its validator. Three cases over raw transport calls:
9
+ * the standard cross suite's generic round-trip never drives it.
10
+ *
11
+ * Its single-request refusals are conformance-table rows on a table given the
12
+ * live mount (`live_rpc_endpoints`): the anonymous 401 a generated auth row,
13
+ * the empty-`ids` 400 a hand-written one. This suite holds what a row can't
14
+ * say, because it names the fixture's own actor:
11
15
  *
12
- * - **anonymous → 401** — the account-grain auth gate refuses an
13
- * unauthenticated caller before the handler runs.
14
16
  * - **keeper resolves own actor → 200** — the populated round trip: the
15
17
  * returned row carries the keeper's `id` + `username`, and **no**
16
18
  * `account_id` / `email` / timestamp / role field (the wire shape's
17
19
  * deliberate info-leak posture). This is the assertion that exercises the
18
20
  * Rust row→JSON mapping against the TS canonical shape.
19
- * - **empty `ids` → 400** — the `min(1)` input bound is enforced on both
20
- * spines (TS Zod, Rust `parse_ids`).
21
21
  *
22
22
  * Runs both legs via the shared `{setup_test}` protocol: the in-process leg
23
23
  * (`auth/actor_lookup_parity.db.test.ts`, plain `gro test`) and the
24
- * cross-process leg (`cross_backend/actor_lookup.cross.test.ts`, the TS spine
24
+ * cross-process leg (`cross_backend/spine_suites.cross.test.ts`, the TS spine
25
25
  * binaries + Rust `testing_spine_stub` over real HTTP). `actor_lookup` is
26
26
  * mounted on every spine, so the suite is ungated.
27
27
  *
@@ -38,15 +38,6 @@ export const describe_actor_lookup_cross_tests = (options) => {
38
38
  const { setup_test } = options;
39
39
  const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
40
40
  describe('actor_lookup parity', () => {
41
- test('anonymous → 401 (account-grain auth gate)', async () => {
42
- const fixture = await setup_test();
43
- const res = await fixture.fresh_transport()(rpc_path, {
44
- method: 'POST',
45
- headers: { 'content-type': 'application/json' },
46
- body: lookup_envelope([fixture.actor.id], 'anon-lookup')
47
- });
48
- assert.strictEqual(res.status, 401, 'unauthenticated actor_lookup must be refused');
49
- });
50
41
  test('keeper resolves own actor → 200 with id + username, no control fields', async () => {
51
42
  const fixture = await setup_test();
52
43
  const res = await fixture.transport(rpc_path, {
@@ -67,14 +58,5 @@ export const describe_actor_lookup_cross_tests = (options) => {
67
58
  assert(!(key in row), `actor_lookup row must not leak '${key}'`);
68
59
  }
69
60
  });
70
- test('empty ids → 400 (min(1) input bound)', async () => {
71
- const fixture = await setup_test();
72
- const res = await fixture.transport(rpc_path, {
73
- method: 'POST',
74
- headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
75
- body: lookup_envelope([], 'empty-lookup')
76
- });
77
- assert.strictEqual(res.status, 400, 'empty ids must fail input validation');
78
- });
79
61
  });
80
62
  };
@@ -1 +1 @@
1
- {"version":3,"file":"app_settings.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/app_settings.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,qFAAqF;AACrF,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AAiBnE,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IA8DxF,CAAC"}
1
+ {"version":3,"file":"app_settings.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/app_settings.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,qFAAqF;AACrF,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AAcnE,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IA8DxF,CAAC"}
@@ -22,7 +22,7 @@ import '../assert_dev_env.js';
22
22
  *
23
23
  * Cites `security.md` §Signup. Runs both legs via the shared `{setup_test}`
24
24
  * protocol: in-process (`auth/app_settings_parity.db.test.ts`) +
25
- * cross-process (`cross_backend/app_settings.cross.test.ts`, TS spine
25
+ * cross-process (`cross_backend/spine_suites.cross.test.ts`, TS spine
26
26
  * binaries + Rust `testing_spine_stub`). Mounted on every spine, so the
27
27
  * suite is ungated.
28
28
  *
@@ -31,9 +31,7 @@ import '../assert_dev_env.js';
31
31
  import { describe, test, assert } from 'vitest';
32
32
  import { app_settings_update_action_spec } from '../../auth/admin_action_specs.js';
33
33
  import { ERROR_NO_MATCHING_INVITE } from '../../http/error_schemas.js';
34
- import { SPINE_RPC_PATH } from './spine_surface_constants.js';
35
- /** REST signup path on the spine surface (`/api/account` prefix + `/signup`). */
36
- const SIGNUP_PATH = '/api/account/signup';
34
+ import { SPINE_ACCOUNT_SIGNUP_PATH, SPINE_RPC_PATH } from './spine_surface_constants.js';
37
35
  /** A password that satisfies the creation-strength schema (min 12). */
38
36
  const SIGNUP_PASSWORD = 'securepassword123';
39
37
  /** Build the JSON-RPC envelope for an `app_settings_update` call. */
@@ -55,7 +53,7 @@ export const describe_app_settings_cross_tests = (options) => {
55
53
  body: update_envelope(true, 'enable-open-signup')
56
54
  });
57
55
  assert.strictEqual(enable.status, 200, 'admin app_settings_update must succeed');
58
- const res = await fixture.fresh_transport()(SIGNUP_PATH, {
56
+ const res = await fixture.fresh_transport()(SPINE_ACCOUNT_SIGNUP_PATH, {
59
57
  method: 'POST',
60
58
  headers: { 'content-type': 'application/json' },
61
59
  body: JSON.stringify({ username: 'open_signup_user', password: SIGNUP_PASSWORD })
@@ -80,7 +78,7 @@ export const describe_app_settings_cross_tests = (options) => {
80
78
  body: update_envelope(false, 'disable-open-signup')
81
79
  });
82
80
  assert.strictEqual(disable.status, 200, 'admin app_settings_update (disable) must succeed');
83
- const res = await fixture.fresh_transport()(SIGNUP_PATH, {
81
+ const res = await fixture.fresh_transport()(SPINE_ACCOUNT_SIGNUP_PATH, {
84
82
  method: 'POST',
85
83
  headers: { 'content-type': 'application/json' },
86
84
  body: JSON.stringify({ username: 'closed_signup_user', password: SIGNUP_PASSWORD })
@@ -0,0 +1,51 @@
1
+ import '../assert_dev_env.js';
2
+ import type { AuditLogEventWithUsernamesJson } from '../../auth/audit_log_schema.js';
3
+ import type { FetchTransport } from '../transports/fetch_transport.js';
4
+ /** The fixture members an audit read uses — every `TestFixture` has them. */
5
+ export interface AuditReadFixture {
6
+ readonly fresh_transport: (options?: {
7
+ readonly origin?: string | null;
8
+ }) => FetchTransport;
9
+ /** The keeper's session headers — the keeper holds the admin role the audit read needs. */
10
+ readonly create_session_headers: (extra?: Record<string, string>) => Record<string, string>;
11
+ readonly create_daemon_token_headers: (extra?: Record<string, string>) => Record<string, string>;
12
+ /** Refuse undeclared metadata keys — see `TestFixture.reject_unknown_audit_metadata_keys`. */
13
+ readonly reject_unknown_audit_metadata_keys?: boolean;
14
+ }
15
+ /**
16
+ * Await the backend's in-flight audit writes via `_testing_drain_effects`, so
17
+ * a following `audit_log_list` sees every row emitted before the call. The
18
+ * backend must mount the action — every spine test binary does; in-process
19
+ * suites mount `create_testing_drain_effects_action`.
20
+ *
21
+ * @param fixture - the per-test fixture, whose daemon token authorizes the drain
22
+ * @param rpc_path - the RPC endpoint the drain action is mounted on
23
+ */
24
+ export declare const cross_drain_effects: (fixture: AuditReadFixture, rpc_path?: string) => Promise<void>;
25
+ /**
26
+ * Assert an audit row's `metadata` carries `expected` — each field compared
27
+ * on its own, so a consumer's added key passes — and none of `absent`. When
28
+ * the fixture's setup closed the world (`reject_unknown_audit_metadata_keys`),
29
+ * the whole object must equal `expected`, so an undeclared key fails too.
30
+ *
31
+ * @param fixture - the per-test fixture, whose flag picks the comparison
32
+ * @param metadata - the row's `metadata`
33
+ * @param expected - the declared fields and their values
34
+ * @param absent - keys the row must not carry, e.g. an optional field left unset
35
+ */
36
+ export declare const assert_audit_metadata_fields: (fixture: Pick<AuditReadFixture, "reject_unknown_audit_metadata_keys">, metadata: Record<string, unknown> | null, expected: Record<string, unknown>, absent?: ReadonlyArray<string>) => void;
37
+ /**
38
+ * Drain the backend's audit writes (`cross_drain_effects`), then list audit rows as
39
+ * the keeper, parse them against `AuditLogListOutput`, and parse each row's
40
+ * `metadata` against its event type's schema (`assert_audit_event_metadata`),
41
+ * with unknown keys refused when `fixture.reject_unknown_audit_metadata_keys`
42
+ * is set. The wire schema leaves `metadata` a loose record, so without this a
43
+ * spine's metadata drift reaches no assertion.
44
+ *
45
+ * @param fixture - the per-test fixture; the list runs on the keeper's session
46
+ * @param params - `audit_log_list` params, e.g. `{event_type: 'login', outcome: 'failure'}`
47
+ * @param rpc_path - the RPC endpoint both actions are mounted on
48
+ * @returns the listed events
49
+ */
50
+ export declare const list_audit_events: (fixture: AuditReadFixture, params: Record<string, unknown>, rpc_path?: string) => Promise<Array<AuditLogEventWithUsernamesJson>>;
51
+ //# sourceMappingURL=audit_cross_helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/audit_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsB9B,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AAErF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAIvE,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,eAAe,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,KAAK,cAAc,CAAC;IAC5F,2FAA2F;IAC3F,QAAQ,CAAC,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5F,QAAQ,CAAC,2BAA2B,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjG,8FAA8F;IAC9F,QAAQ,CAAC,kCAAkC,CAAC,EAAE,OAAO,CAAC;CACtD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC/B,SAAS,gBAAgB,EACzB,WAAU,MAAuB,KAC/B,OAAO,CAAC,IAAI,CASd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,IAAI,CAAC,gBAAgB,EAAE,oCAAoC,CAAC,EACrE,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACxC,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,SAAQ,aAAa,CAAC,MAAM,CAAM,KAChC,IASF,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,GAC7B,SAAS,gBAAgB,EACzB,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAU,MAAuB,KAC/B,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAe/C,CAAC"}
@@ -0,0 +1,78 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Audit-row reads for the cross-backend suites, behind the
4
+ * `_testing_drain_effects` barrier.
5
+ *
6
+ * Both spines write audit rows off the request path — a WS mutation is
7
+ * answered before its effects flush, and the Rust spine's emits are
8
+ * fire-and-forget — so a suite that reads `audit_log_list` right after the
9
+ * call it audits can miss the row, and an assertion that a row is absent then
10
+ * passes vacuously. `list_audit_events` drains before it lists, so no reader
11
+ * can skip the barrier, and holds every row it lists to its event type's
12
+ * metadata schema (`assert_audit_event_metadata`), so every reader checks the
13
+ * metadata of the rows it reads, on every spine — closed to undeclared keys
14
+ * when the fixture's setup asked for it.
15
+ *
16
+ * @module
17
+ */
18
+ import { assert } from 'vitest';
19
+ import { AuditLogListOutput } from '../../auth/admin_action_specs.js';
20
+ import { assert_audit_event_metadata } from '../audit_drift_guard.js';
21
+ import { cross_rpc_call, expect_output } from './cross_rpc.js';
22
+ import { SPINE_RPC_PATH } from './spine_surface_constants.js';
23
+ /**
24
+ * Await the backend's in-flight audit writes via `_testing_drain_effects`, so
25
+ * a following `audit_log_list` sees every row emitted before the call. The
26
+ * backend must mount the action — every spine test binary does; in-process
27
+ * suites mount `create_testing_drain_effects_action`.
28
+ *
29
+ * @param fixture - the per-test fixture, whose daemon token authorizes the drain
30
+ * @param rpc_path - the RPC endpoint the drain action is mounted on
31
+ */
32
+ export const cross_drain_effects = async (fixture, rpc_path = SPINE_RPC_PATH) => {
33
+ const drained = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
34
+ assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
35
+ };
36
+ /**
37
+ * Assert an audit row's `metadata` carries `expected` — each field compared
38
+ * on its own, so a consumer's added key passes — and none of `absent`. When
39
+ * the fixture's setup closed the world (`reject_unknown_audit_metadata_keys`),
40
+ * the whole object must equal `expected`, so an undeclared key fails too.
41
+ *
42
+ * @param fixture - the per-test fixture, whose flag picks the comparison
43
+ * @param metadata - the row's `metadata`
44
+ * @param expected - the declared fields and their values
45
+ * @param absent - keys the row must not carry, e.g. an optional field left unset
46
+ */
47
+ export const assert_audit_metadata_fields = (fixture, metadata, expected, absent = []) => {
48
+ assert.ok(metadata, 'the audit row carries metadata');
49
+ for (const [key, value] of Object.entries(expected)) {
50
+ assert.deepStrictEqual(metadata[key], value, `metadata.${key}`);
51
+ }
52
+ for (const key of absent)
53
+ assert.notProperty(metadata, key, `metadata.${key} is absent`);
54
+ if (fixture.reject_unknown_audit_metadata_keys) {
55
+ assert.deepStrictEqual(metadata, expected, 'metadata carries only the declared fields');
56
+ }
57
+ };
58
+ /**
59
+ * Drain the backend's audit writes (`cross_drain_effects`), then list audit rows as
60
+ * the keeper, parse them against `AuditLogListOutput`, and parse each row's
61
+ * `metadata` against its event type's schema (`assert_audit_event_metadata`),
62
+ * with unknown keys refused when `fixture.reject_unknown_audit_metadata_keys`
63
+ * is set. The wire schema leaves `metadata` a loose record, so without this a
64
+ * spine's metadata drift reaches no assertion.
65
+ *
66
+ * @param fixture - the per-test fixture; the list runs on the keeper's session
67
+ * @param params - `audit_log_list` params, e.g. `{event_type: 'login', outcome: 'failure'}`
68
+ * @param rpc_path - the RPC endpoint both actions are mounted on
69
+ * @returns the listed events
70
+ */
71
+ export const list_audit_events = async (fixture, params, rpc_path = SPINE_RPC_PATH) => {
72
+ await cross_drain_effects(fixture, rpc_path);
73
+ const { events } = expect_output(await cross_rpc_call(fixture.fresh_transport(), rpc_path, 'audit_log_list', params, fixture.create_session_headers()), AuditLogListOutput);
74
+ const reject_unknown_keys = fixture.reject_unknown_audit_metadata_keys ?? false;
75
+ for (const event of events)
76
+ assert_audit_event_metadata(event, { reject_unknown_keys });
77
+ return events;
78
+ };
@@ -44,7 +44,8 @@ export interface BackendBootstrapConfig {
44
44
  * (env: `*_DAEMON_TOKEN_PATH`). `spawn_backend` reads this file once
45
45
  * after the health probe succeeds and threads the token onto
46
46
  * `BackendHandle.daemon_token` for `_testing_reset` calls plus any
47
- * other admin/keeper-gated cross-process tests.
47
+ * other admin/keeper-gated cross-process tests. `spawn_backend` deletes
48
+ * the file before each spawn, so a token a previous run left is never read.
48
49
  */
49
50
  readonly daemon_token_path: string;
50
51
  }
@@ -1 +1 @@
1
- {"version":3,"file":"backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACtC,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,sBAAsB,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;CAC3C"}
1
+ {"version":3,"file":"backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACtC,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,sBAAsB,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;CAC3C"}
@@ -20,6 +20,12 @@ export interface BodySizeCrossTestOptions extends RpcPathCrossSuiteOptions {
20
20
  * time.
21
21
  */
22
22
  readonly base_url: string | null;
23
+ /**
24
+ * The backend's request-body cap on `rpc_path`, in bytes — the boundary the
25
+ * cases probe. Default `SPINE_BODY_LIMIT_DEFAULT_BYTES`; pass the backend's
26
+ * value when it caps its RPC path elsewhere.
27
+ */
28
+ readonly rpc_max_body_bytes?: number;
23
29
  }
24
30
  export declare const describe_body_size_cross_tests: (options: BodySizeCrossTestOptions) => void;
25
31
  //# sourceMappingURL=body_size.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"body_size.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6F9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AAGxE;;;;;GAKG;AACH,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACzE;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAwHD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IA+HlF,CAAC"}
1
+ {"version":3,"file":"body_size.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiG9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AAGxE;;;;;GAKG;AACH,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACzE;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AA8FD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IAkIlF,CAAC"}
@@ -4,7 +4,8 @@ import '../assert_dev_env.js';
4
4
  *
5
5
  * `create_app_server` (TS) and the Rust spine both cap the request body at a
6
6
  * 1 MiB default (`DEFAULT_MAX_BODY_SIZE` / `fuz_http`'s
7
- * `DEFAULT_BODY_LIMIT_BYTES`) and reject oversized payloads with `413` and the
7
+ * `DEFAULT_BODY_LIMIT_BYTES`; a backend capping its RPC path elsewhere passes
8
+ * `rpc_max_body_bytes`) and reject oversized payloads with `413` and the
8
9
  * canonical flat REST body `{error: 'payload_too_large'}`. A declared
9
10
  * `Content-Length` over the cap is refused on the header, *before* auth,
10
11
  * origin, or dispatch run (middleware step 4). Each impl unit-tests this in
@@ -55,7 +56,7 @@ import '../assert_dev_env.js';
55
56
  *
56
57
  * - a **declared** over-cap length is sent as the request head alone —
57
58
  * `Content-Length` one over the cap and not one body byte. The 413 must
58
- * arrive within `RAW_READ_TIMEOUT_MS`, which also pins that the refusal is
59
+ * arrive within `RAW_HTTP_READ_TIMEOUT_MS`, which also pins that the refusal is
59
60
  * made on the header: a backend that reads the body before refusing, or has
60
61
  * no cap, waits for bytes that never come and fails the case.
61
62
  * - the **chunked** body is sent up to one byte past the cap and never
@@ -72,7 +73,7 @@ import '../assert_dev_env.js';
72
73
  * imperative suite, not a `conformance_table` row. Runs both legs via the
73
74
  * shared `setup_test` protocol: the in-process leg
74
75
  * (`auth/body_size_parity.db.test.ts`, plain `gro test`) and the cross-process
75
- * leg (`cross_backend/body_size.cross.test.ts`, the TS spine binaries + Rust
76
+ * leg (`cross_backend/spine_suites.cross.test.ts`, the TS spine binaries + Rust
76
77
  * `testing_spine_stub` over real HTTP). The body-size limit is on every spine,
77
78
  * so the suite is ungated.
78
79
  *
@@ -83,16 +84,8 @@ import '../assert_dev_env.js';
83
84
  import { describe, test, assert } from 'vitest';
84
85
  import { account_verify_action_spec } from '../../auth/account_action_specs.js';
85
86
  import { ERROR_PAYLOAD_TOO_LARGE } from '../../http/error_schemas.js';
86
- import { raw_http_exchange, raw_http_parse_response } from '../transports/raw_http.js';
87
- import { SPINE_RPC_PATH } from './spine_surface_constants.js';
88
- /**
89
- * The shared 1 MiB default both impls ship (`DEFAULT_MAX_BODY_SIZE` in TS,
90
- * `DEFAULT_BODY_LIMIT_BYTES` in `fuz_http`). Kept as a local cross-impl
91
- * contract value rather than imported from either impl — the boundary it
92
- * pins is the shared default, and a change to either is a deliberate
93
- * cross-impl decision that would revisit this suite regardless.
94
- */
95
- const BODY_LIMIT_DEFAULT_BYTES = 1024 * 1024;
87
+ import { raw_http_exchange, raw_http_header_lines, raw_http_parse_json, raw_http_parse_response, RAW_HTTP_READ_TIMEOUT_MS } from '../transports/raw_http.js';
88
+ import { SPINE_BODY_LIMIT_DEFAULT_BYTES, SPINE_RPC_PATH } from './spine_surface_constants.js';
96
89
  /**
97
90
  * Build an `account_verify` JSON-RPC envelope whose serialized length is
98
91
  * *exactly* `target_bytes`, by growing a `params.pad` string to fill. Every
@@ -101,6 +94,10 @@ const BODY_LIMIT_DEFAULT_BYTES = 1024 * 1024;
101
94
  * middleware measures. `account_verify` takes no params, but at/over the cap
102
95
  * the size check fires before input validation, so the padded shape is never
103
96
  * reached on the rejection path.
97
+ *
98
+ * The envelope with an empty pad is already 70-odd bytes (more with a longer
99
+ * `id`), so a smaller target overshoots: the body comes out at that floor, not
100
+ * at `target_bytes`.
104
101
  */
105
102
  const sized_envelope = (id, target_bytes) => {
106
103
  const method = account_verify_action_spec.method;
@@ -131,28 +128,10 @@ const streamed_body = (total_bytes) => {
131
128
  }
132
129
  });
133
130
  };
134
- /**
135
- * How long a raw over-cap exchange waits for the refusal. The server has every
136
- * byte it will get when the wait starts, so a backend that refuses on what it
137
- * has answers at once and one that waits for more runs the whole window.
138
- */
139
- const RAW_READ_TIMEOUT_MS = 2000;
140
131
  const to_sized_response = async (res) => ({
141
132
  status: res.status,
142
133
  body: await res.json().catch(() => undefined)
143
134
  });
144
- const parse_json = (text) => {
145
- try {
146
- return JSON.parse(text);
147
- }
148
- catch {
149
- return undefined;
150
- }
151
- };
152
- /** Render request headers as raw header lines. */
153
- const to_header_lines = (headers) => Object.entries(headers)
154
- .map(([name, value]) => `${name}: ${value}\r\n`)
155
- .join('');
156
135
  /**
157
136
  * Write `request_bytes` to the backend's socket and read the one response it
158
137
  * must answer with — without being sent anything more.
@@ -161,17 +140,17 @@ const to_header_lines = (headers) => Object.entries(headers)
161
140
  */
162
141
  const exchange_withholding = async (base_url, request_bytes, withheld) => {
163
142
  const { received, closed_by_server } = await raw_http_exchange(base_url, request_bytes, {
164
- read_timeout_ms: RAW_READ_TIMEOUT_MS,
143
+ read_timeout_ms: RAW_HTTP_READ_TIMEOUT_MS,
165
144
  until: 'response'
166
145
  });
167
146
  const response = raw_http_parse_response(received);
168
147
  assert.ok(response, `expected a whole response with ${withheld} withheld; ` +
169
148
  (closed_by_server
170
149
  ? `the server closed the connection first`
171
- : `none arrived within ${RAW_READ_TIMEOUT_MS}ms — the backend is waiting for bytes ` +
150
+ : `none arrived within ${RAW_HTTP_READ_TIMEOUT_MS}ms — the backend is waiting for bytes ` +
172
151
  `it should refuse without`) +
173
152
  `. Received: ${received ? received.slice(0, 120) : 'nothing'}`);
174
- return { status: response.status, body: parse_json(response.body) };
153
+ return { status: response.status, body: raw_http_parse_json(response.body) };
175
154
  };
176
155
  /** Assert a response is the canonical over-cap refusal. */
177
156
  const assert_payload_too_large = (res, message) => {
@@ -181,6 +160,7 @@ const assert_payload_too_large = (res, message) => {
181
160
  export const describe_body_size_cross_tests = (options) => {
182
161
  const { setup_test, base_url } = options;
183
162
  const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
163
+ const cap = options.rpc_max_body_bytes ?? SPINE_BODY_LIMIT_DEFAULT_BYTES;
184
164
  /** POST `body` to the RPC path as the keeper. */
185
165
  const post = (fixture, body) => fixture.transport(rpc_path, {
186
166
  method: 'POST',
@@ -194,7 +174,7 @@ export const describe_body_size_cross_tests = (options) => {
194
174
  */
195
175
  const post_over_limit_declared = async (fixture, id) => {
196
176
  const headers = fixture.create_session_headers();
197
- const content_length = BODY_LIMIT_DEFAULT_BYTES + 1;
177
+ const content_length = cap + 1;
198
178
  if (base_url === null) {
199
179
  return to_sized_response(await fixture.transport(rpc_path, {
200
180
  method: 'POST',
@@ -204,7 +184,7 @@ export const describe_body_size_cross_tests = (options) => {
204
184
  }
205
185
  const url = new URL(base_url);
206
186
  return exchange_withholding(base_url, `POST ${rpc_path} HTTP/1.1\r\n` +
207
- to_header_lines({
187
+ raw_http_header_lines({
208
188
  host: url.host,
209
189
  origin: url.origin,
210
190
  ...headers,
@@ -223,18 +203,19 @@ export const describe_body_size_cross_tests = (options) => {
223
203
  return to_sized_response(await fixture.transport(rpc_path, {
224
204
  method: 'POST',
225
205
  headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
226
- body: streamed_body(BODY_LIMIT_DEFAULT_BYTES + 1024),
206
+ body: streamed_body(cap + 1024),
227
207
  // a streamed request body needs half-duplex `fetch`
228
208
  duplex: 'half'
229
209
  }));
230
210
  }
231
211
  const url = new URL(base_url);
232
- // whole chunks up to exactly the cap, then a one-byte chunk's size line
233
- // and its byte — and neither that chunk's closing CRLF nor the
234
- // terminating chunk, so the last byte sent is the first one past the cap
235
- const whole_chunk = `${STREAM_CHUNK_BYTES.toString(16)}\r\n${'x'.repeat(STREAM_CHUNK_BYTES)}\r\n`;
212
+ // chunks up to exactly the cap, then a one-byte chunk's size line and its
213
+ // byte — and neither that chunk's closing CRLF nor the terminating chunk,
214
+ // so the last byte sent is the first one past the cap
215
+ const to_chunk = (size) => `${size.toString(16)}\r\n${'x'.repeat(size)}\r\n`;
216
+ const remainder = cap % STREAM_CHUNK_BYTES;
236
217
  return exchange_withholding(base_url, `POST ${rpc_path} HTTP/1.1\r\n` +
237
- to_header_lines({
218
+ raw_http_header_lines({
238
219
  host: url.host,
239
220
  origin: url.origin,
240
221
  ...fixture.create_session_headers(),
@@ -242,7 +223,8 @@ export const describe_body_size_cross_tests = (options) => {
242
223
  'transfer-encoding': 'chunked'
243
224
  }) +
244
225
  `\r\n` +
245
- whole_chunk.repeat(BODY_LIMIT_DEFAULT_BYTES / STREAM_CHUNK_BYTES) +
226
+ to_chunk(STREAM_CHUNK_BYTES).repeat(Math.floor(cap / STREAM_CHUNK_BYTES)) +
227
+ (remainder === 0 ? '' : to_chunk(remainder)) +
246
228
  `1\r\nx`, `the chunked body's end`);
247
229
  };
248
230
  describe('body-size limit parity', () => {
@@ -259,7 +241,7 @@ export const describe_body_size_cross_tests = (options) => {
259
241
  // Exactly at the cap is one byte under the `>` threshold, so it clears
260
242
  // the size gate and reaches the dispatcher. Whatever the dispatcher
261
243
  // then returns, it must not be the 413 — that's the lower boundary.
262
- const res = await post(fixture, sized_envelope('at-limit', BODY_LIMIT_DEFAULT_BYTES));
244
+ const res = await post(fixture, sized_envelope('at-limit', cap));
263
245
  assert.notStrictEqual(res.status, 413, 'a body exactly at the cap must not be size-rejected');
264
246
  });
265
247
  test('under-limit POST (small) → 200', async () => {
@@ -5,6 +5,11 @@ export interface BodySizeSmugglingCrossTestOptions {
5
5
  readonly base_url: string;
6
6
  /** RPC endpoint path to target. Default `/api/rpc`. */
7
7
  readonly rpc_path?: string;
8
+ /**
9
+ * The backend's request-body cap on `rpc_path`, in bytes; the oversized
10
+ * `POST` declares a length past it. Default `SPINE_BODY_LIMIT_DEFAULT_BYTES`.
11
+ */
12
+ readonly rpc_max_body_bytes?: number;
8
13
  }
9
14
  export declare const describe_body_size_smuggling_cross_tests: (options: BodySizeSmugglingCrossTestOptions) => void;
10
15
  //# sourceMappingURL=body_size_smuggling.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmE9B,4EAA4E;AAC5E,MAAM,WAAW,iCAAiC;IACjD,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAWD,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IA4GF,CAAC"}
1
+ {"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuE9B,4EAA4E;AAC5E,MAAM,WAAW,iCAAiC;IACjD,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IA4GF,CAAC"}