@fuzdev/fuz_app 0.147.0 → 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 (273) 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/cell_action_specs.d.ts +3 -1
  7. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  8. package/dist/auth/cell_action_specs.js +5 -5
  9. package/dist/auth/cell_actions.d.ts.map +1 -1
  10. package/dist/auth/cell_actions.js +2 -3
  11. package/dist/auth/cell_data_schema.d.ts +1 -1
  12. package/dist/auth/cell_data_schema.js +1 -1
  13. package/dist/auth/cell_field_action_specs.d.ts +4 -3
  14. package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
  15. package/dist/auth/cell_field_action_specs.js +5 -6
  16. package/dist/auth/cell_field_actions.d.ts.map +1 -1
  17. package/dist/auth/cell_field_actions.js +8 -1
  18. package/dist/auth/cell_item_action_specs.d.ts +4 -2
  19. package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
  20. package/dist/auth/cell_item_action_specs.js +5 -5
  21. package/dist/auth/cell_item_actions.d.ts.map +1 -1
  22. package/dist/auth/cell_item_actions.js +8 -1
  23. package/dist/auth/role_grant_offer_action_specs.d.ts +2 -0
  24. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  25. package/dist/auth/role_grant_offer_action_specs.js +3 -1
  26. package/dist/primitive_schemas.d.ts +1 -1
  27. package/dist/primitive_schemas.d.ts.map +1 -1
  28. package/dist/primitive_schemas.js +2 -1
  29. package/dist/schema_meta.d.ts +20 -0
  30. package/dist/schema_meta.d.ts.map +1 -1
  31. package/dist/schema_meta.js +7 -1
  32. package/dist/testing/CLAUDE.md +720 -114
  33. package/dist/testing/admin_integration.d.ts.map +1 -1
  34. package/dist/testing/admin_integration.js +73 -5
  35. package/dist/testing/adversarial_input.d.ts +0 -47
  36. package/dist/testing/adversarial_input.d.ts.map +1 -1
  37. package/dist/testing/adversarial_input.js +4 -340
  38. package/dist/testing/adversarial_input_cases.d.ts +64 -0
  39. package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
  40. package/dist/testing/adversarial_input_cases.js +348 -0
  41. package/dist/testing/audit_completeness.d.ts.map +1 -1
  42. package/dist/testing/audit_completeness.js +24 -20
  43. package/dist/testing/audit_drift_guard.d.ts +27 -1
  44. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  45. package/dist/testing/audit_drift_guard.js +50 -0
  46. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  47. package/dist/testing/cross_backend/account_lifecycle.js +29 -12
  48. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  49. package/dist/testing/cross_backend/actor_lookup.js +7 -25
  50. package/dist/testing/cross_backend/app_settings.d.ts.map +1 -1
  51. package/dist/testing/cross_backend/app_settings.js +4 -6
  52. package/dist/testing/cross_backend/audit_cross_helpers.d.ts +51 -0
  53. package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
  54. package/dist/testing/cross_backend/audit_cross_helpers.js +78 -0
  55. package/dist/testing/cross_backend/backend_config.d.ts +2 -1
  56. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -1
  57. package/dist/testing/cross_backend/body_size.d.ts +6 -0
  58. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  59. package/dist/testing/cross_backend/body_size.js +26 -44
  60. package/dist/testing/cross_backend/body_size_smuggling.d.ts +5 -0
  61. package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
  62. package/dist/testing/cross_backend/body_size_smuggling.js +7 -14
  63. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +3 -1
  64. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/capabilities.d.ts +55 -59
  66. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  67. package/dist/testing/cross_backend/capabilities.js +20 -23
  68. package/dist/testing/cross_backend/cargo_binary.d.ts +70 -0
  69. package/dist/testing/cross_backend/cargo_binary.d.ts.map +1 -0
  70. package/dist/testing/cross_backend/cargo_binary.js +148 -0
  71. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  72. package/dist/testing/cross_backend/cell_crud.js +57 -1
  73. package/dist/testing/cross_backend/cell_gated_create.d.ts +0 -60
  74. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  75. package/dist/testing/cross_backend/cell_gated_create.js +9 -487
  76. package/dist/testing/cross_backend/cell_grant_role.d.ts +14 -2
  77. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  78. package/dist/testing/cross_backend/cell_grant_role.js +22 -12
  79. package/dist/testing/cross_backend/cell_moderate.d.ts +66 -0
  80. package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -0
  81. package/dist/testing/cross_backend/cell_moderate.js +511 -0
  82. package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
  83. package/dist/testing/cross_backend/cell_ref_admission.js +1 -1
  84. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  85. package/dist/testing/cross_backend/cell_relations.js +40 -19
  86. package/dist/testing/cross_backend/conformance_case.d.ts +43 -4
  87. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  88. package/dist/testing/cross_backend/conformance_case.js +29 -1
  89. package/dist/testing/cross_backend/conformance_rpc_auth.d.ts +93 -0
  90. package/dist/testing/cross_backend/conformance_rpc_auth.d.ts.map +1 -0
  91. package/dist/testing/cross_backend/conformance_rpc_auth.js +125 -0
  92. package/dist/testing/cross_backend/conformance_table.d.ts +144 -4
  93. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  94. package/dist/testing/cross_backend/conformance_table.js +297 -65
  95. package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -1
  96. package/dist/testing/cross_backend/cookie_attributes.js +3 -2
  97. package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +38 -5
  98. package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts.map +1 -1
  99. package/dist/testing/cross_backend/create_cross_backend_global_setup.js +24 -3
  100. package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +5 -5
  101. package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts.map +1 -1
  102. package/dist/testing/cross_backend/credential_deadline.d.ts +10 -6
  103. package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -1
  104. package/dist/testing/cross_backend/credential_deadline.js +32 -31
  105. package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
  106. package/dist/testing/cross_backend/credential_header_robustness.js +4 -6
  107. package/dist/testing/cross_backend/{cell_cross_helpers.d.ts → cross_rpc.d.ts} +12 -2
  108. package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -0
  109. package/dist/testing/cross_backend/{cell_cross_helpers.js → cross_rpc.js} +31 -7
  110. package/dist/testing/cross_backend/default_backend_configs.d.ts +56 -61
  111. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  112. package/dist/testing/cross_backend/default_backend_configs.js +97 -91
  113. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
  114. package/dist/testing/cross_backend/default_spine_surface.js +2 -2
  115. package/dist/testing/cross_backend/differential.d.ts +126 -0
  116. package/dist/testing/cross_backend/differential.d.ts.map +1 -0
  117. package/dist/testing/cross_backend/differential.js +201 -0
  118. package/dist/testing/cross_backend/differential_normalize.d.ts +150 -0
  119. package/dist/testing/cross_backend/differential_normalize.d.ts.map +1 -0
  120. package/dist/testing/cross_backend/differential_normalize.js +153 -0
  121. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  122. package/dist/testing/cross_backend/fact_serving.js +27 -17
  123. package/dist/testing/cross_backend/full_spine_mount.d.ts +15 -0
  124. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  125. package/dist/testing/cross_backend/full_spine_mount.js +55 -1
  126. package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
  127. package/dist/testing/cross_backend/identity_parity.js +6 -5
  128. package/dist/testing/cross_backend/in_process_setup.d.ts +12 -2
  129. package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
  130. package/dist/testing/cross_backend/in_process_setup.js +10 -7
  131. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  132. package/dist/testing/cross_backend/login_security.js +13 -18
  133. package/dist/testing/cross_backend/origin.js +1 -1
  134. package/dist/testing/cross_backend/origin_conformance.js +1 -1
  135. package/dist/testing/cross_backend/origin_scope.d.ts +6 -0
  136. package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -1
  137. package/dist/testing/cross_backend/origin_scope.js +13 -12
  138. package/dist/testing/cross_backend/peer_ping_ws.d.ts +2 -5
  139. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  140. package/dist/testing/cross_backend/peer_ping_ws.js +14 -24
  141. package/dist/testing/cross_backend/postgres_database.d.ts +85 -0
  142. package/dist/testing/cross_backend/postgres_database.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/postgres_database.js +140 -0
  144. package/dist/testing/cross_backend/provided_context.d.ts +28 -0
  145. package/dist/testing/cross_backend/provided_context.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/provided_context.js +35 -0
  147. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
  148. package/dist/testing/cross_backend/query_shape.js +10 -12
  149. package/dist/testing/cross_backend/ready.js +2 -2
  150. package/dist/testing/cross_backend/revocation_drivers.d.ts +4 -3
  151. package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -1
  152. package/dist/testing/cross_backend/revocation_drivers.js +1 -1
  153. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +3 -2
  154. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -1
  155. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts +13 -0
  156. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -0
  157. package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +203 -0
  158. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +2 -5
  159. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  160. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +203 -111
  161. package/dist/testing/cross_backend/role_grant_participation.d.ts +13 -0
  162. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
  163. package/dist/testing/cross_backend/role_grant_participation.js +32 -21
  164. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -1
  165. package/dist/testing/cross_backend/role_grant_role_grammar.js +6 -9
  166. package/dist/testing/cross_backend/rpc_adversarial.d.ts +38 -0
  167. package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
  168. package/dist/testing/cross_backend/rpc_adversarial.js +292 -0
  169. package/dist/testing/cross_backend/rpc_success_fixtures.d.ts +24 -0
  170. package/dist/testing/cross_backend/rpc_success_fixtures.d.ts.map +1 -0
  171. package/dist/testing/cross_backend/rpc_success_fixtures.js +124 -0
  172. package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +101 -0
  173. package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts.map +1 -0
  174. package/dist/testing/cross_backend/rust_spine_consumer_surface.js +113 -0
  175. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +10 -0
  176. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  177. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +27 -18
  178. package/dist/testing/cross_backend/session_cap.d.ts +9 -8
  179. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  180. package/dist/testing/cross_backend/session_cap.js +9 -4
  181. package/dist/testing/cross_backend/session_cookie_codec.js +1 -1
  182. package/dist/testing/cross_backend/setup.d.ts +56 -54
  183. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  184. package/dist/testing/cross_backend/setup.js +29 -29
  185. package/dist/testing/cross_backend/simple_request.d.ts.map +1 -1
  186. package/dist/testing/cross_backend/simple_request.js +4 -4
  187. package/dist/testing/cross_backend/spawn_backend.d.ts +59 -10
  188. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
  189. package/dist/testing/cross_backend/spawn_backend.js +219 -60
  190. package/dist/testing/cross_backend/spine_suites.d.ts +231 -0
  191. package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
  192. package/dist/testing/cross_backend/spine_suites.js +626 -0
  193. package/dist/testing/cross_backend/spine_surface_constants.d.ts +29 -0
  194. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  195. package/dist/testing/cross_backend/spine_surface_constants.js +29 -0
  196. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  197. package/dist/testing/cross_backend/sse_round_trip.js +3 -4
  198. package/dist/testing/cross_backend/standard.d.ts +18 -1
  199. package/dist/testing/cross_backend/standard.d.ts.map +1 -1
  200. package/dist/testing/cross_backend/standard.js +2 -0
  201. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +24 -0
  202. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  203. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +15 -0
  204. package/dist/testing/cross_backend/testing_backdoor.d.ts +2 -8
  205. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  206. package/dist/testing/cross_backend/testing_backdoor.js +7 -10
  207. package/dist/testing/cross_backend/testing_reset_actions.d.ts +3 -2
  208. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  209. package/dist/testing/cross_backend/testing_reset_actions.js +8 -4
  210. package/dist/testing/cross_backend/testing_server_bun.js +1 -1
  211. package/dist/testing/cross_backend/token_lifetime.js +1 -1
  212. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  213. package/dist/testing/cross_backend/token_scope_surface.js +9 -13
  214. package/dist/testing/cross_backend/transport_composition.d.ts +26 -2
  215. package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -1
  216. package/dist/testing/cross_backend/transport_composition.js +81 -51
  217. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +8 -0
  218. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  219. package/dist/testing/cross_backend/ts_spine_backend_config.js +17 -10
  220. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +2 -5
  221. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
  222. package/dist/testing/cross_backend/ws_action_rate_limit.js +8 -14
  223. package/dist/testing/cross_backend/ws_connection_cap.d.ts +4 -9
  224. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -1
  225. package/dist/testing/cross_backend/ws_connection_cap.js +11 -12
  226. package/dist/testing/cross_backend/ws_endpoint.d.ts +52 -0
  227. package/dist/testing/cross_backend/ws_endpoint.d.ts.map +1 -0
  228. package/dist/testing/cross_backend/ws_endpoint.js +54 -0
  229. package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +2 -5
  230. package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -1
  231. package/dist/testing/cross_backend/ws_inbound_dispatch.js +9 -15
  232. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +2 -7
  233. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
  234. package/dist/testing/cross_backend/ws_outbound_overflow.js +5 -9
  235. package/dist/testing/cross_backend/ws_role_gated.d.ts +4 -5
  236. package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -1
  237. package/dist/testing/cross_backend/ws_role_gated.js +12 -21
  238. package/dist/testing/cross_backend/ws_round_trip.d.ts +12 -9
  239. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  240. package/dist/testing/cross_backend/ws_round_trip.js +29 -34
  241. package/dist/testing/db_entities.d.ts +16 -3
  242. package/dist/testing/db_entities.d.ts.map +1 -1
  243. package/dist/testing/db_entities.js +28 -5
  244. package/dist/testing/integration_helpers.d.ts +61 -2
  245. package/dist/testing/integration_helpers.d.ts.map +1 -1
  246. package/dist/testing/integration_helpers.js +75 -18
  247. package/dist/testing/round_trip.d.ts +17 -0
  248. package/dist/testing/round_trip.d.ts.map +1 -1
  249. package/dist/testing/round_trip.js +67 -24
  250. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  251. package/dist/testing/rpc_attack_surface.js +13 -138
  252. package/dist/testing/rpc_envelope_cases.d.ts +111 -0
  253. package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
  254. package/dist/testing/rpc_envelope_cases.js +240 -0
  255. package/dist/testing/rpc_helpers.d.ts +31 -0
  256. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  257. package/dist/testing/rpc_helpers.js +35 -14
  258. package/dist/testing/rpc_round_trip.d.ts +62 -6
  259. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  260. package/dist/testing/rpc_round_trip.js +151 -39
  261. package/dist/testing/schema_generators.d.ts.map +1 -1
  262. package/dist/testing/schema_generators.js +22 -2
  263. package/dist/testing/standard.d.ts +26 -0
  264. package/dist/testing/standard.d.ts.map +1 -1
  265. package/dist/testing/standard.js +4 -1
  266. package/dist/testing/transports/raw_http.d.ts +21 -0
  267. package/dist/testing/transports/raw_http.d.ts.map +1 -1
  268. package/dist/testing/transports/raw_http.js +30 -0
  269. package/package.json +6 -4
  270. package/dist/testing/cross_backend/actor_search.d.ts +0 -6
  271. package/dist/testing/cross_backend/actor_search.d.ts.map +0 -1
  272. package/dist/testing/cross_backend/actor_search.js +0 -90
  273. package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +0 -1
@@ -0,0 +1,348 @@
1
+ import './assert_dev_env.js';
2
+ /**
3
+ * Schema-walk generation of adversarial inputs — payloads that must fail
4
+ * validation.
5
+ *
6
+ * Focused cases: one representative wrong-type value per field, one format
7
+ * violation per constrained field, one null and one missing case per required
8
+ * field, plus whole-body structural attacks (non-object body, extra unknown
9
+ * keys). Hono-free, so the cross-process suites reuse the generators the
10
+ * in-process `describe_adversarial_input` and `describe_rpc_attack_surface_tests`
11
+ * run.
12
+ *
13
+ * @module
14
+ */
15
+ import { z } from 'zod';
16
+ import { zod_unwrap_to_object, zod_extract_fields } from '../zod.js';
17
+ import { is_null_schema } from '../http/schema_helpers.js';
18
+ import { ERROR_INVALID_REQUEST_BODY, ERROR_INVALID_JSON_BODY } from '../http/error_schemas.js';
19
+ import { detect_format, generate_valid_value } from './schema_generators.js';
20
+ // --- Payload generation ---
21
+ /** One wrong-type value per base type — one representative is sufficient. */
22
+ const wrong_type_for = (base_type) => {
23
+ switch (base_type) {
24
+ case 'string':
25
+ case 'uuid':
26
+ case 'email':
27
+ return { label: 'number instead of string', value: 42 };
28
+ case 'number':
29
+ case 'int':
30
+ return { label: 'string instead of number', value: 'not_a_number' };
31
+ case 'boolean':
32
+ return { label: 'number instead of boolean', value: 42 };
33
+ case 'array':
34
+ return { label: 'string instead of array', value: 'not_an_array' };
35
+ case 'object':
36
+ return { label: 'string instead of object', value: 'not_an_object' };
37
+ case 'enum':
38
+ return { label: 'number instead of enum', value: 42 };
39
+ default:
40
+ return null;
41
+ }
42
+ };
43
+ /** Format violation payloads — one per constraint type. */
44
+ const format_violation = (format) => {
45
+ switch (format) {
46
+ case 'uuid':
47
+ return { label: 'malformed uuid', value: 'not-a-uuid' };
48
+ case 'email':
49
+ return { label: 'malformed email', value: 'not-an-email' };
50
+ case 'date-time':
51
+ return { label: 'malformed datetime', value: 'not-a-date' };
52
+ case 'pattern':
53
+ return { label: 'pattern violation', value: "'; DROP TABLE --" };
54
+ default:
55
+ return null;
56
+ }
57
+ };
58
+ // --- Input test case generation ---
59
+ /**
60
+ * Generate adversarial test cases for a route's input schema.
61
+ *
62
+ * Produces focused, non-redundant cases:
63
+ * - Whole-body: send array instead of object, extra unknown key
64
+ * - Missing required fields (without defaults)
65
+ * - One wrong-type value per field
66
+ * - Null for required non-nullable fields
67
+ * - One format violation per constrained field
68
+ *
69
+ * @throws Error if the seed body built by `generate_valid_value` fails
70
+ * `input_schema.safeParse` — surfaces broken generation logic with the
71
+ * Zod issues path rather than producing nonsense adversarial cases.
72
+ */
73
+ export const generate_input_test_cases = (input_schema) => {
74
+ if (is_null_schema(input_schema))
75
+ return [];
76
+ const object_schema = zod_unwrap_to_object(input_schema);
77
+ if (!object_schema)
78
+ return [];
79
+ const fields = zod_extract_fields(object_schema);
80
+ const cases = [];
81
+ // build a base object with valid-ish values — must pass validation itself
82
+ // skip optional fields without defaults to avoid generating invalid nested values
83
+ const base = {};
84
+ for (const field of fields) {
85
+ if (!field.required && !field.has_default)
86
+ continue;
87
+ base[field.name] = generate_valid_value(field, object_schema.shape[field.name]);
88
+ }
89
+ let base_result = input_schema.safeParse(base);
90
+ if (!base_result.success) {
91
+ // Fallback for schemas with a top-level `.refine()` that requires at
92
+ // least one of N optional fields. Fill optional fields until the base
93
+ // satisfies validation; surfaces a clear error if no combination works.
94
+ for (const field of fields) {
95
+ if (field.required || field.has_default || field.name in base)
96
+ continue;
97
+ base[field.name] = generate_valid_value(field, object_schema.shape[field.name]);
98
+ base_result = input_schema.safeParse(base);
99
+ if (base_result.success)
100
+ break;
101
+ }
102
+ }
103
+ if (!base_result.success) {
104
+ throw new Error(`adversarial_input: generated base object fails validation for schema — ` +
105
+ `fix generate_valid_value for: ${base_result.error.issues
106
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
107
+ .join(', ')}`);
108
+ }
109
+ // whole-body structural: send array instead of object
110
+ cases.push({
111
+ label: 'non-object body (array)',
112
+ body: [1, 2, 3],
113
+ expected_error: ERROR_INVALID_JSON_BODY
114
+ });
115
+ // whole-body structural: extra unknown key (enforces strictObject)
116
+ // only emit if the schema rejects unknown keys (i.e. uses z.strictObject)
117
+ const extra_key_result = input_schema.safeParse({ ...base, __adversarial_extra: 'rejected' });
118
+ if (!extra_key_result.success) {
119
+ cases.push({
120
+ label: 'extra unknown key',
121
+ body: { ...base, __adversarial_extra: 'should_be_rejected' },
122
+ expected_error: ERROR_INVALID_REQUEST_BODY
123
+ });
124
+ }
125
+ for (const field of fields) {
126
+ const field_schema = object_schema.shape[field.name];
127
+ // missing required field (skip fields with defaults — Zod fills them)
128
+ if (field.required && !field.has_default) {
129
+ const without = {};
130
+ for (const [k, v] of Object.entries(base)) {
131
+ if (k !== field.name)
132
+ without[k] = v;
133
+ }
134
+ cases.push({
135
+ label: `missing: ${field.name}`,
136
+ body: without,
137
+ expected_error: ERROR_INVALID_REQUEST_BODY
138
+ });
139
+ }
140
+ // one wrong-type value
141
+ const wrong = wrong_type_for(field.base_type);
142
+ if (wrong) {
143
+ cases.push({
144
+ label: `wrong type: ${field.name} (${wrong.label})`,
145
+ body: { ...base, [field.name]: wrong.value },
146
+ expected_error: ERROR_INVALID_REQUEST_BODY
147
+ });
148
+ }
149
+ // null for required non-nullable fields
150
+ if (field.required && !field.nullable && field.base_type !== 'null') {
151
+ cases.push({
152
+ label: `null: ${field.name}`,
153
+ body: { ...base, [field.name]: null },
154
+ expected_error: ERROR_INVALID_REQUEST_BODY
155
+ });
156
+ }
157
+ // format violation for constrained string fields
158
+ const format = detect_format(field_schema);
159
+ if (format) {
160
+ const violation = format_violation(format);
161
+ if (violation) {
162
+ cases.push({
163
+ label: `format: ${field.name} (${violation.label})`,
164
+ body: { ...base, [field.name]: violation.value },
165
+ expected_error: ERROR_INVALID_REQUEST_BODY
166
+ });
167
+ }
168
+ }
169
+ // --- Boundary cases via JSON Schema introspection ---
170
+ try {
171
+ const json = z.toJSONSchema(field_schema);
172
+ // string length boundaries
173
+ if (field.base_type === 'string' ||
174
+ field.base_type === 'uuid' ||
175
+ field.base_type === 'email') {
176
+ if (typeof json.minLength === 'number' && json.minLength > 0) {
177
+ cases.push({
178
+ label: `empty string: ${field.name}`,
179
+ body: { ...base, [field.name]: '' },
180
+ expected_error: ERROR_INVALID_REQUEST_BODY
181
+ });
182
+ }
183
+ if (typeof json.maxLength === 'number') {
184
+ cases.push({
185
+ label: `over maxLength: ${field.name} (${json.maxLength + 1} chars)`,
186
+ body: { ...base, [field.name]: 'x'.repeat(json.maxLength + 1) },
187
+ expected_error: ERROR_INVALID_REQUEST_BODY
188
+ });
189
+ }
190
+ }
191
+ // numeric boundaries
192
+ if (field.base_type === 'number' || field.base_type === 'int') {
193
+ if (typeof json.minimum === 'number') {
194
+ cases.push({
195
+ label: `below minimum: ${field.name} (${json.minimum - 1})`,
196
+ body: { ...base, [field.name]: json.minimum - 1 },
197
+ expected_error: ERROR_INVALID_REQUEST_BODY
198
+ });
199
+ }
200
+ if (typeof json.maximum === 'number') {
201
+ cases.push({
202
+ label: `above maximum: ${field.name} (${json.maximum + 1})`,
203
+ body: { ...base, [field.name]: json.maximum + 1 },
204
+ expected_error: ERROR_INVALID_REQUEST_BODY
205
+ });
206
+ }
207
+ if (typeof json.exclusiveMinimum === 'number') {
208
+ cases.push({
209
+ label: `at exclusive minimum: ${field.name} (${json.exclusiveMinimum})`,
210
+ body: { ...base, [field.name]: json.exclusiveMinimum },
211
+ expected_error: ERROR_INVALID_REQUEST_BODY
212
+ });
213
+ }
214
+ if (typeof json.exclusiveMaximum === 'number') {
215
+ cases.push({
216
+ label: `at exclusive maximum: ${field.name} (${json.exclusiveMaximum})`,
217
+ body: { ...base, [field.name]: json.exclusiveMaximum },
218
+ expected_error: ERROR_INVALID_REQUEST_BODY
219
+ });
220
+ }
221
+ // 0 and negative for positive-only fields
222
+ if (typeof json.minimum === 'number' && json.minimum > 0) {
223
+ cases.push({
224
+ label: `zero for positive-only: ${field.name}`,
225
+ body: { ...base, [field.name]: 0 },
226
+ expected_error: ERROR_INVALID_REQUEST_BODY
227
+ });
228
+ cases.push({
229
+ label: `negative for positive-only: ${field.name}`,
230
+ body: { ...base, [field.name]: -1 },
231
+ expected_error: ERROR_INVALID_REQUEST_BODY
232
+ });
233
+ }
234
+ }
235
+ // array length boundaries
236
+ if (field.base_type === 'array') {
237
+ if (typeof json.minItems === 'number' && json.minItems > 0) {
238
+ cases.push({
239
+ label: `empty array for minItems > 0: ${field.name}`,
240
+ body: { ...base, [field.name]: [] },
241
+ expected_error: ERROR_INVALID_REQUEST_BODY
242
+ });
243
+ }
244
+ if (typeof json.maxItems === 'number') {
245
+ // generate an array one item over the max (items are null — schema-agnostic)
246
+ cases.push({
247
+ label: `over maxItems: ${field.name} (${json.maxItems + 1} items)`,
248
+ body: { ...base, [field.name]: Array.from({ length: json.maxItems + 1 }, () => null) },
249
+ expected_error: ERROR_INVALID_REQUEST_BODY
250
+ });
251
+ }
252
+ }
253
+ }
254
+ catch {
255
+ // schema can't be converted to JSON Schema, skip boundary cases
256
+ }
257
+ }
258
+ return cases;
259
+ };
260
+ // --- Params test case generation ---
261
+ /**
262
+ * Generate adversarial test cases for a route's params schema.
263
+ *
264
+ * Params are always strings from URL segments. Only generates cases for
265
+ * format-constrained fields (uuid, pattern) since unconstrained string
266
+ * params accept any string value.
267
+ */
268
+ export const generate_params_test_cases = (params_schema) => {
269
+ const fields = zod_extract_fields(params_schema);
270
+ const cases = [];
271
+ // build base params with valid-ish values
272
+ const base_params = {};
273
+ for (const field of fields) {
274
+ const field_schema = params_schema.shape[field.name];
275
+ const format = detect_format(field_schema);
276
+ if (format === 'uuid' || field.base_type === 'uuid') {
277
+ base_params[field.name] = '00000000-0000-0000-0000-000000000000';
278
+ }
279
+ else {
280
+ base_params[field.name] = 'test_value';
281
+ }
282
+ }
283
+ for (const field of fields) {
284
+ const field_schema = params_schema.shape[field.name];
285
+ const format = detect_format(field_schema);
286
+ if (!format)
287
+ continue; // unconstrained string — any value passes
288
+ const violation = format_violation(format);
289
+ if (violation) {
290
+ cases.push({
291
+ label: `${violation.label}: param ${field.name}`,
292
+ params: { ...base_params, [field.name]: violation.value }
293
+ });
294
+ }
295
+ }
296
+ return cases;
297
+ };
298
+ // --- Query test case generation ---
299
+ /**
300
+ * Generate adversarial test cases for a route's query schema.
301
+ *
302
+ * Query params are always strings from the URL. Generates cases for:
303
+ * - Missing required fields
304
+ * - Format violations on constrained fields (uuid, pattern)
305
+ */
306
+ export const generate_query_test_cases = (query_schema) => {
307
+ const fields = zod_extract_fields(query_schema);
308
+ const cases = [];
309
+ // build base query with valid-ish values
310
+ const base_query = {};
311
+ for (const field of fields) {
312
+ const field_schema = query_schema.shape[field.name];
313
+ const format = detect_format(field_schema);
314
+ if (format === 'uuid' || field.base_type === 'uuid') {
315
+ base_query[field.name] = '00000000-0000-0000-0000-000000000000';
316
+ }
317
+ else {
318
+ base_query[field.name] = 'test_value';
319
+ }
320
+ }
321
+ for (const field of fields) {
322
+ // missing required field
323
+ if (field.required && !field.has_default) {
324
+ const without = {};
325
+ for (const [k, v] of Object.entries(base_query)) {
326
+ if (k !== field.name)
327
+ without[k] = v;
328
+ }
329
+ cases.push({
330
+ label: `missing query: ${field.name}`,
331
+ query: without
332
+ });
333
+ }
334
+ // format violation for constrained string fields
335
+ const field_schema = query_schema.shape[field.name];
336
+ const format = detect_format(field_schema);
337
+ if (!format)
338
+ continue;
339
+ const violation = format_violation(format);
340
+ if (violation) {
341
+ cases.push({
342
+ label: `${violation.label}: query ${field.name}`,
343
+ query: { ...base_query, [field.name]: violation.value }
344
+ });
345
+ }
346
+ }
347
+ return cases;
348
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"audit_completeness.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_completeness.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA4B7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAUhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AA2B1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,0BAA0B,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C;;;;;OAKG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;OAMG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,yEAAyE;IACzE,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;OAGG;IACH,aAAa,EAAE,uBAAuB,CAAC;CACvC;AAgGD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,4BAA4B,KAAG,IAgnBzF,CAAC"}
1
+ {"version":3,"file":"audit_completeness.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_completeness.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA4B7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAUhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AA2B1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,0BAA0B,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C;;;;;OAKG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;OAMG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,yEAAyE;IACzE,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;OAGG;IACH,aAAa,EAAE,uBAAuB,CAAC;CACvC;AAoGD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,4BAA4B,KAAG,IAgnBzF,CAAC"}
@@ -54,8 +54,12 @@ const create_admin_observer = (fixture) => fixture.create_account({ username: 'a
54
54
  * `observer` is a dedicated admin account (see `create_admin_observer`)
55
55
  * — its credentials are never the subject of the mutation under test, so the
56
56
  * read works uniformly across every flow including session-revoking ones.
57
+ *
58
+ * In-process only, so no drain precedes the read (`create_test_app` settles a
59
+ * request's effects before it responds) — the cross-process reader is
60
+ * `cross_backend/audit_cross_helpers.ts`'s `list_audit_events`.
57
61
  */
58
- const list_audit_events = async (app, rpc_path, observer, params = {}) => {
62
+ const read_audit_log = async (app, rpc_path, observer, params = {}) => {
59
63
  const res = await rpc_call_for_spec({
60
64
  app,
61
65
  path: rpc_path,
@@ -128,7 +132,7 @@ export const describe_audit_completeness_tests = (options) => {
128
132
  })
129
133
  });
130
134
  assert.strictEqual(res.status, 200);
131
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
135
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
132
136
  assert_has_event(events, 'login', 'POST /login (success)');
133
137
  });
134
138
  test('login failure produces login event with failure outcome', async () => {
@@ -145,7 +149,7 @@ export const describe_audit_completeness_tests = (options) => {
145
149
  })
146
150
  });
147
151
  assert.strictEqual(res.status, 401);
148
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
152
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
149
153
  assert_has_event(events, 'login', 'POST /login (failure)');
150
154
  });
151
155
  test('logout produces logout event', async () => {
@@ -158,7 +162,7 @@ export const describe_audit_completeness_tests = (options) => {
158
162
  headers: fixture.create_session_headers({ 'content-type': 'application/json' })
159
163
  });
160
164
  assert.strictEqual(res.status, 200);
161
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
165
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
162
166
  assert_has_event(events, 'logout', 'POST /logout');
163
167
  });
164
168
  test('token create produces token_create event', async () => {
@@ -176,7 +180,7 @@ export const describe_audit_completeness_tests = (options) => {
176
180
  headers: fixture.create_session_headers()
177
181
  });
178
182
  assert.ok(res.ok, `account_token_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
179
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
183
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
180
184
  assert_has_event(events, 'token_create', 'account_token_create RPC');
181
185
  assert_event_credential_type(events, 'token_create', 'session', 'account_token_create RPC');
182
186
  });
@@ -202,7 +206,7 @@ export const describe_audit_completeness_tests = (options) => {
202
206
  headers: fixture.create_session_headers()
203
207
  });
204
208
  assert.ok(res.ok, `account_token_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
205
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
209
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
206
210
  assert_has_event(events, 'token_revoke', 'account_token_revoke RPC');
207
211
  assert_event_credential_type(events, 'token_revoke', 'session', 'account_token_revoke RPC');
208
212
  });
@@ -242,7 +246,7 @@ export const describe_audit_completeness_tests = (options) => {
242
246
  headers: fixture.create_session_headers()
243
247
  });
244
248
  assert.ok(res.ok, `account_session_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
245
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
249
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
246
250
  assert_has_event(events, 'session_revoke', 'account_session_revoke RPC');
247
251
  assert_event_credential_type(events, 'session_revoke', 'session', 'account_session_revoke RPC');
248
252
  });
@@ -257,7 +261,7 @@ export const describe_audit_completeness_tests = (options) => {
257
261
  headers: fixture.create_session_headers()
258
262
  });
259
263
  assert.ok(res.ok, `account_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
260
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
264
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
261
265
  assert_has_event(events, 'session_revoke_all', 'account_session_revoke_all RPC');
262
266
  assert_event_credential_type(events, 'session_revoke_all', 'session', 'account_session_revoke_all RPC');
263
267
  });
@@ -275,7 +279,7 @@ export const describe_audit_completeness_tests = (options) => {
275
279
  })
276
280
  });
277
281
  assert.strictEqual(res.status, 200);
278
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
282
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
279
283
  assert_has_event(events, 'password_change', 'POST /password');
280
284
  assert_event_credential_type(events, 'password_change', 'session', 'POST /password');
281
285
  });
@@ -298,7 +302,7 @@ export const describe_audit_completeness_tests = (options) => {
298
302
  // Admin offer emits `role_grant_offer_create` only — the role_grant doesn't
299
303
  // exist yet. Drive the accept to confirm `role_grant_offer_accept` and
300
304
  // `role_grant_create` both fire on the downstream consent transition.
301
- const events_after_offer = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
305
+ const events_after_offer = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
302
306
  assert_has_event(events_after_offer, 'role_grant_offer_create', 'role_grant_offer_create RPC');
303
307
  const accept_res = await rpc_call_for_spec({
304
308
  app: { request: fixture.transport },
@@ -308,7 +312,7 @@ export const describe_audit_completeness_tests = (options) => {
308
312
  headers: target.create_session_headers()
309
313
  });
310
314
  assert.ok(accept_res.ok, `role_grant_offer_accept failed: ${accept_res.ok ? '' : JSON.stringify(accept_res.error)}`);
311
- const events_after_accept = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
315
+ const events_after_accept = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
312
316
  assert_has_event(events_after_accept, 'role_grant_offer_accept', 'offer accept RPC');
313
317
  assert_has_event(events_after_accept, 'role_grant_create', 'offer accept RPC');
314
318
  });
@@ -335,7 +339,7 @@ export const describe_audit_completeness_tests = (options) => {
335
339
  headers: fixture.create_session_headers()
336
340
  });
337
341
  assert.ok(revoke_res.ok, `role_grant_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
338
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
342
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
339
343
  assert_has_event(events, 'role_grant_revoke', 'role_grant_revoke RPC');
340
344
  // Audit envelope must populate both target columns —
341
345
  // `role_grant_revoke` is the canonical actor-bound-subject event.
@@ -357,7 +361,7 @@ export const describe_audit_completeness_tests = (options) => {
357
361
  headers: fixture.create_session_headers()
358
362
  });
359
363
  assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
360
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
364
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
361
365
  // admin session revoke-all also produces session_revoke_all
362
366
  assert_has_event(events, 'session_revoke_all', 'admin_session_revoke_all RPC');
363
367
  });
@@ -373,7 +377,7 @@ export const describe_audit_completeness_tests = (options) => {
373
377
  headers: fixture.create_session_headers()
374
378
  });
375
379
  assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
376
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
380
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
377
381
  assert_has_event(events, 'token_revoke_all', 'admin_token_revoke_all RPC');
378
382
  });
379
383
  });
@@ -399,7 +403,7 @@ export const describe_audit_completeness_tests = (options) => {
399
403
  headers: fixture.create_session_headers()
400
404
  });
401
405
  assert.ok(delete_res.ok, `invite_delete failed: ${delete_res.ok ? '' : JSON.stringify(delete_res.error)}`);
402
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
406
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
403
407
  assert_has_event(events, 'invite_create', 'invite_create RPC');
404
408
  assert_has_event(events, 'invite_delete', 'invite_delete RPC');
405
409
  });
@@ -417,7 +421,7 @@ export const describe_audit_completeness_tests = (options) => {
417
421
  headers: fixture.create_session_headers()
418
422
  });
419
423
  assert.ok(res.ok, `app_settings_update failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
420
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
424
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
421
425
  assert_has_event(events, 'app_settings_update', 'app_settings_update RPC');
422
426
  });
423
427
  });
@@ -435,7 +439,7 @@ export const describe_audit_completeness_tests = (options) => {
435
439
  headers: fixture.create_session_headers()
436
440
  });
437
441
  assert.ok(res.ok, `account_delete failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
438
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
442
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
439
443
  assert_has_event(events, 'account_delete', 'account_delete RPC');
440
444
  assert_has_event(events, 'actor_delete', 'account_delete RPC (per-actor cascade)');
441
445
  });
@@ -455,7 +459,7 @@ export const describe_audit_completeness_tests = (options) => {
455
459
  headers: fixture.create_daemon_token_headers()
456
460
  });
457
461
  assert.ok(res.ok, `account_purge failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
458
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
462
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
459
463
  assert_has_event(events, 'account_purge', 'account_purge RPC');
460
464
  assert_has_event(events, 'actor_purge', 'account_purge RPC (per-actor cascade)');
461
465
  });
@@ -479,7 +483,7 @@ export const describe_audit_completeness_tests = (options) => {
479
483
  headers: fixture.create_session_headers()
480
484
  });
481
485
  assert.ok(res.ok, `account_undelete failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
482
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
486
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
483
487
  assert_has_event(events, 'account_undelete', 'account_undelete RPC');
484
488
  assert_has_event(events, 'actor_undelete', 'account_undelete RPC (per-actor cascade)');
485
489
  });
@@ -514,7 +518,7 @@ export const describe_audit_completeness_tests = (options) => {
514
518
  })
515
519
  });
516
520
  assert.strictEqual(res.status, 200);
517
- const events = await list_audit_events({ request: fixture.transport }, rpc_path, observer);
521
+ const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
518
522
  assert_has_event(events, 'signup', 'POST /signup');
519
523
  });
520
524
  });
@@ -1,6 +1,6 @@
1
1
  import './assert_dev_env.js';
2
2
  import { type AuditEmitter } from '../auth/audit_emitter.js';
3
- import type { AuditLogInput } from '../auth/audit_log_schema.js';
3
+ import { type AuditLogEventJson, type AuditLogInput } from '../auth/audit_log_schema.js';
4
4
  /**
5
5
  * Register per-test `beforeEach` + `afterEach` hooks that catch any audit
6
6
  * emission with a metadata shape that fails its `audit_metadata_schemas`
@@ -81,4 +81,30 @@ export interface RecordingAuditEmitter {
81
81
  * so the freeze isn't load-bearing here.
82
82
  */
83
83
  export declare const create_recording_audit_emitter: (calls_ref?: Array<AuditLogInput>) => RecordingAuditEmitter;
84
+ /** Options for `assert_audit_event_metadata`. */
85
+ export interface AssertAuditEventMetadataOptions {
86
+ /**
87
+ * Refuse metadata keys the event type doesn't declare. Off by default: the
88
+ * builtin schemas admit consumer-added fields. For a suite whose backend
89
+ * adds none — a spine's own — so an undeclared key on any spine fails.
90
+ */
91
+ readonly reject_unknown_keys?: boolean;
92
+ }
93
+ /**
94
+ * Assert an audit row read off the wire carries the metadata its event type
95
+ * declares — parsed against `audit_metadata_schemas[event_type]` as declared,
96
+ * so a mistyped, `null`, or missing field fails, and with
97
+ * `reject_unknown_keys` so does a key the event doesn't declare. The wire
98
+ * schema (`AuditLogEventJson.metadata`) is a loose record, and only the TS
99
+ * spine validates metadata, at insert and fail-open, so this is the check that
100
+ * holds a spine's rows to their event's contract.
101
+ *
102
+ * Builtin event types only: a consumer-registered type has no entry here, and
103
+ * passes unexamined.
104
+ *
105
+ * @param event - an audit row from `audit_log_list` or an audit read like it
106
+ * @param options - `reject_unknown_keys` for the closed-world check
107
+ * @throws AssertionError naming the event type and the schema issues
108
+ */
109
+ export declare const assert_audit_event_metadata: (event: Pick<AuditLogEventJson, "event_type" | "outcome" | "metadata">, options?: AssertAuditEventMetadataOptions) => void;
84
110
  //# sourceMappingURL=audit_drift_guard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"audit_drift_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_drift_guard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,EAGN,KAAK,YAAY,EACjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAiB,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEhF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAO5C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,QAAO,IAgBxC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,8BAA8B,GAC1C,YAAY,KAAK,CAAC,aAAa,CAAC,KAC9B,qBAqCF,CAAC"}
1
+ {"version":3,"file":"audit_drift_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_drift_guard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAW7B,OAAO,EAGN,KAAK,YAAY,EACjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAGN,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAO5C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,QAAO,IAgBxC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,8BAA8B,GAC1C,YAAY,KAAK,CAAC,aAAa,CAAC,KAC9B,qBAqCF,CAAC;AA2BF,iDAAiD;AACjD,MAAM,WAAW,+BAA+B;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,2BAA2B,GACvC,OAAO,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,SAAS,GAAG,UAAU,CAAC,EACrE,UAAU,+BAA+B,KACvC,IAcF,CAAC"}
@@ -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"}