@kirrosh/zond 0.21.0 → 0.23.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 (256) hide show
  1. package/CHANGELOG.md +758 -3
  2. package/README.md +78 -15
  3. package/package.json +17 -10
  4. package/src/cli/argv.ts +122 -0
  5. package/src/cli/commands/add-api.ts +134 -0
  6. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  7. package/src/cli/commands/api/annotate/index.ts +525 -0
  8. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  9. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  10. package/src/cli/commands/api/annotate/pagination.ts +60 -0
  11. package/src/cli/commands/api/annotate/prompts.ts +183 -0
  12. package/src/cli/commands/api/annotate/readback.ts +58 -0
  13. package/src/cli/commands/api/annotate/resources.ts +91 -0
  14. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  15. package/src/cli/commands/audit.ts +480 -0
  16. package/src/cli/commands/bootstrap.ts +710 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +348 -0
  19. package/src/cli/commands/checks.ts +756 -0
  20. package/src/cli/commands/ci-init.ts +55 -6
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +262 -0
  23. package/src/cli/commands/completions.ts +192 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +180 -8
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1236 -0
  28. package/src/cli/commands/doctor.ts +607 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +420 -47
  31. package/src/cli/commands/init/agents-md.ts +61 -0
  32. package/src/cli/commands/init/bootstrap.ts +108 -0
  33. package/src/cli/commands/init/index.ts +244 -0
  34. package/src/cli/commands/init/skills.ts +98 -0
  35. package/src/cli/commands/init/templates/agents.md +77 -0
  36. package/src/cli/commands/init/templates/markdown.d.ts +4 -0
  37. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +651 -0
  40. package/src/cli/commands/init/templates/zond-config.yml +14 -0
  41. package/src/cli/commands/prepare-fixtures.ts +135 -0
  42. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  43. package/src/cli/commands/probe/security.ts +454 -0
  44. package/src/cli/commands/probe/static.ts +255 -0
  45. package/src/cli/commands/probe/webhooks.ts +161 -0
  46. package/src/cli/commands/probe.ts +459 -0
  47. package/src/cli/commands/reference.ts +87 -0
  48. package/src/cli/commands/refresh-api.ts +169 -0
  49. package/src/cli/commands/remove-api.ts +150 -0
  50. package/src/cli/commands/report-bundle.ts +318 -0
  51. package/src/cli/commands/report.ts +241 -0
  52. package/src/cli/commands/request.ts +379 -4
  53. package/src/cli/commands/run.ts +911 -33
  54. package/src/cli/commands/session.ts +244 -0
  55. package/src/cli/commands/use.ts +74 -0
  56. package/src/cli/index.ts +36 -607
  57. package/src/cli/json-envelope.ts +112 -3
  58. package/src/cli/json-schemas.ts +263 -0
  59. package/src/cli/program.ts +218 -0
  60. package/src/cli/resolve.ts +105 -0
  61. package/src/cli/status-filter.ts +124 -0
  62. package/src/cli/util/api-context.ts +85 -0
  63. package/src/cli/version.ts +8 -0
  64. package/src/core/anti-fp/bootstrap.ts +34 -0
  65. package/src/core/anti-fp/index.ts +33 -0
  66. package/src/core/anti-fp/registry.ts +44 -0
  67. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  68. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  69. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  70. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  71. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  72. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  73. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  74. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  75. package/src/core/anti-fp/types.ts +68 -0
  76. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  77. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  78. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  79. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  80. package/src/core/checks/checks/cross_call_references.ts +134 -0
  81. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  82. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  83. package/src/core/checks/checks/ignored_auth.ts +211 -0
  84. package/src/core/checks/checks/index.ts +65 -0
  85. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  86. package/src/core/checks/checks/missing_required_header.ts +40 -0
  87. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  88. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  89. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  90. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  91. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  92. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  93. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  94. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  95. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  96. package/src/core/checks/checks/unsupported_method.ts +63 -0
  97. package/src/core/checks/checks/use_after_free.ts +78 -0
  98. package/src/core/checks/index.ts +30 -0
  99. package/src/core/checks/mode.ts +79 -0
  100. package/src/core/checks/recommended-action.ts +64 -0
  101. package/src/core/checks/registry.ts +78 -0
  102. package/src/core/checks/runner.ts +874 -0
  103. package/src/core/checks/sarif.ts +230 -0
  104. package/src/core/checks/stateful.ts +121 -0
  105. package/src/core/checks/types.ts +189 -0
  106. package/src/core/classifier/recommended-action.ts +222 -0
  107. package/src/core/context/current.ts +51 -0
  108. package/src/core/context/session.ts +78 -0
  109. package/src/core/coverage/loader.ts +185 -0
  110. package/src/core/coverage/reasons.ts +300 -0
  111. package/src/core/diagnostics/db-analysis.ts +161 -12
  112. package/src/core/diagnostics/failure-class.ts +120 -0
  113. package/src/core/diagnostics/failure-hints.ts +212 -9
  114. package/src/core/diagnostics/spec-pointer.ts +99 -0
  115. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  116. package/src/core/exporter/case-study/index.ts +270 -0
  117. package/src/core/exporter/curl.ts +40 -0
  118. package/src/core/exporter/exporter.ts +48 -0
  119. package/src/core/exporter/html-report/escape.ts +24 -0
  120. package/src/core/exporter/html-report/index.ts +479 -0
  121. package/src/core/exporter/html-report/script.ts +100 -0
  122. package/src/core/exporter/html-report/styles.ts +408 -0
  123. package/src/core/generator/chunker.ts +53 -15
  124. package/src/core/generator/coverage-phase.ts +0 -0
  125. package/src/core/generator/create-body.ts +89 -0
  126. package/src/core/generator/data-factory.ts +490 -33
  127. package/src/core/generator/describe.ts +1 -1
  128. package/src/core/generator/fixtures-builder.ts +325 -0
  129. package/src/core/generator/index.ts +7 -5
  130. package/src/core/generator/openapi-reader.ts +55 -3
  131. package/src/core/generator/path-param-disambig.ts +114 -0
  132. package/src/core/generator/resources-builder.ts +648 -0
  133. package/src/core/generator/schema-utils.ts +11 -3
  134. package/src/core/generator/serializer.ts +114 -15
  135. package/src/core/generator/suite-generator.ts +484 -77
  136. package/src/core/generator/types.ts +8 -0
  137. package/src/core/identity/identity-file.ts +129 -0
  138. package/src/core/lint/affects.ts +28 -0
  139. package/src/core/lint/config.ts +96 -0
  140. package/src/core/lint/format.ts +42 -0
  141. package/src/core/lint/index.ts +94 -0
  142. package/src/core/lint/reporter.ts +128 -0
  143. package/src/core/lint/rules/consistency.ts +158 -0
  144. package/src/core/lint/rules/heuristics.ts +97 -0
  145. package/src/core/lint/rules/strictness.ts +109 -0
  146. package/src/core/lint/types.ts +96 -0
  147. package/src/core/lint/walker.ts +248 -0
  148. package/src/core/meta/meta-store.ts +6 -73
  149. package/src/core/output/README.md +91 -0
  150. package/src/core/output/index.ts +13 -0
  151. package/src/core/output/run.ts +126 -0
  152. package/src/core/output/types.ts +129 -0
  153. package/src/core/parser/env-interpolation.ts +104 -0
  154. package/src/core/parser/filter.ts +57 -0
  155. package/src/core/parser/schema.ts +132 -5
  156. package/src/core/parser/types.ts +29 -2
  157. package/src/core/parser/variables.ts +0 -0
  158. package/src/core/parser/yaml-parser.ts +108 -13
  159. package/src/core/probe/bootstrap.ts +34 -0
  160. package/src/core/probe/dry-run-envelope.ts +57 -0
  161. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  162. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  163. package/src/core/probe/mass-assignment-template.ts +212 -0
  164. package/src/core/probe/method-probe.ts +164 -0
  165. package/src/core/probe/method-shared.ts +69 -0
  166. package/src/core/probe/negative-probe.ts +691 -0
  167. package/src/core/probe/orphan-tracker.ts +188 -0
  168. package/src/core/probe/path-discovery.ts +440 -0
  169. package/src/core/probe/probe-harness.ts +120 -0
  170. package/src/core/probe/registry.ts +89 -0
  171. package/src/core/probe/runner.ts +136 -0
  172. package/src/core/probe/security-probe-class.ts +201 -0
  173. package/src/core/probe/security-probe.ts +1453 -0
  174. package/src/core/probe/shared.ts +505 -0
  175. package/src/core/probe/static-probe-class.ts +125 -0
  176. package/src/core/probe/types.ts +165 -0
  177. package/src/core/probe/verdict-aggregator.ts +33 -0
  178. package/src/core/probe/webhooks-probe.ts +284 -0
  179. package/src/core/reporter/console.ts +69 -4
  180. package/src/core/reporter/index.ts +2 -3
  181. package/src/core/reporter/json.ts +15 -2
  182. package/src/core/reporter/junit.ts +27 -12
  183. package/src/core/reporter/ndjson.ts +37 -0
  184. package/src/core/reporter/types.ts +3 -0
  185. package/src/core/runner/assertions.ts +62 -2
  186. package/src/core/runner/async-pool.ts +108 -0
  187. package/src/core/runner/auth-path.ts +8 -0
  188. package/src/core/runner/ci-context.ts +72 -0
  189. package/src/core/runner/executor.ts +391 -52
  190. package/src/core/runner/form-encode.ts +51 -0
  191. package/src/core/runner/http-client.ts +115 -7
  192. package/src/core/runner/learn-drift.ts +293 -0
  193. package/src/core/runner/preflight-vars.ts +149 -0
  194. package/src/core/runner/progress-tracker.ts +73 -0
  195. package/src/core/runner/rate-limiter.ts +203 -0
  196. package/src/core/runner/run-kind.ts +39 -0
  197. package/src/core/runner/schema-validator.ts +312 -0
  198. package/src/core/runner/send-request.ts +153 -20
  199. package/src/core/runner/types.ts +38 -0
  200. package/src/core/secrets/registry.ts +164 -0
  201. package/src/core/secrets/secrets-file.ts +115 -0
  202. package/src/core/selectors/operation-filter.ts +144 -0
  203. package/src/core/setup-api.ts +419 -17
  204. package/src/core/severity/category.ts +94 -0
  205. package/src/core/severity/index.ts +121 -0
  206. package/src/core/spec/layers.ts +154 -0
  207. package/src/core/util/format-eta.ts +21 -0
  208. package/src/core/utils.ts +5 -1
  209. package/src/core/workspace/config.ts +129 -0
  210. package/src/core/workspace/manifest.ts +283 -0
  211. package/src/core/workspace/output-rotation.ts +62 -0
  212. package/src/core/workspace/root.ts +94 -0
  213. package/src/core/workspace/triage-path.ts +87 -0
  214. package/src/db/lint-runs.ts +47 -0
  215. package/src/db/migrate.ts +126 -0
  216. package/src/db/migrations/0001_run_kind.sql +25 -0
  217. package/src/db/migrations/sql.d.ts +4 -0
  218. package/src/db/queries/collections.ts +133 -0
  219. package/src/db/queries/coverage.ts +9 -0
  220. package/src/db/queries/dashboard.ts +59 -0
  221. package/src/db/queries/results.ts +128 -0
  222. package/src/db/queries/runs.ts +235 -0
  223. package/src/db/queries/sessions.ts +42 -0
  224. package/src/db/queries/settings.ts +28 -0
  225. package/src/db/queries/types.ts +172 -0
  226. package/src/db/queries.ts +72 -802
  227. package/src/db/schema.ts +179 -48
  228. package/src/cli/commands/export.ts +0 -144
  229. package/src/cli/commands/guide.ts +0 -127
  230. package/src/cli/commands/init.ts +0 -57
  231. package/src/cli/commands/serve.ts +0 -81
  232. package/src/cli/commands/sync.ts +0 -269
  233. package/src/cli/commands/update.ts +0 -189
  234. package/src/cli/commands/validate.ts +0 -34
  235. package/src/core/exporter/postman.ts +0 -963
  236. package/src/core/generator/guide-builder.ts +0 -253
  237. package/src/core/meta/types.ts +0 -21
  238. package/src/core/parser/index.ts +0 -21
  239. package/src/core/runner/execute-run.ts +0 -132
  240. package/src/core/runner/index.ts +0 -12
  241. package/src/core/sync/spec-differ.ts +0 -38
  242. package/src/web/data/collection-state.ts +0 -362
  243. package/src/web/routes/api.ts +0 -314
  244. package/src/web/routes/dashboard.ts +0 -350
  245. package/src/web/routes/runs.ts +0 -64
  246. package/src/web/schemas.ts +0 -121
  247. package/src/web/server.ts +0 -134
  248. package/src/web/static/htmx.min.cjs +0 -1
  249. package/src/web/static/style.css +0 -1148
  250. package/src/web/views/endpoints-tab.ts +0 -174
  251. package/src/web/views/explorer-tab.ts +0 -402
  252. package/src/web/views/health-strip.ts +0 -92
  253. package/src/web/views/layout.ts +0 -48
  254. package/src/web/views/results.ts +0 -210
  255. package/src/web/views/runs-tab.ts +0 -126
  256. package/src/web/views/suites-tab.ts +0 -181
@@ -1,21 +1,22 @@
1
1
  import { resolve } from "path";
2
+ import type { SourceMetadata } from "../parser/types.ts";
2
3
 
3
4
  // ──────────────────────────────────────────────
4
5
  // Utility functions (moved from skeleton.ts)
5
6
  // ──────────────────────────────────────────────
6
7
 
7
- export function isRelativeUrl(url: string): boolean {
8
+ function isRelativeUrl(url: string): boolean {
8
9
  return url.startsWith("/") && !url.includes("://");
9
10
  }
10
11
 
11
- export function resolveSpecPath(specPath: string): string {
12
+ function resolveSpecPath(specPath: string): string {
12
13
  if (specPath.startsWith("http://") || specPath.startsWith("https://")) {
13
14
  return specPath;
14
15
  }
15
16
  return resolve(specPath);
16
17
  }
17
18
 
18
- export function sanitizeEnvName(name: string): string {
19
+ function sanitizeEnvName(name: string): string {
19
20
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
20
21
  }
21
22
 
@@ -25,9 +26,10 @@ export function sanitizeEnvName(name: string): string {
25
26
 
26
27
  export interface RawStep {
27
28
  name: string;
29
+ source?: SourceMetadata;
28
30
  [methodKey: string]: unknown;
29
31
  expect: {
30
- status?: number;
32
+ status?: number | number[];
31
33
  body?: Record<string, Record<string, string>>;
32
34
  headers?: Record<string, unknown>;
33
35
  };
@@ -37,6 +39,7 @@ export interface RawSuite {
37
39
  name: string;
38
40
  setup?: boolean;
39
41
  tags?: string[];
42
+ source?: SourceMetadata;
40
43
  folder?: string;
41
44
  fileStem?: string;
42
45
  base_url?: string;
@@ -66,11 +69,20 @@ export function serializeSuite(suite: RawSuite): string {
66
69
  lines.push(` ${hk}: ${yamlScalar(String(hv))}`);
67
70
  }
68
71
  }
72
+ if (suite.source && Object.keys(suite.source).length > 0) {
73
+ lines.push("source:");
74
+ serializeValue(suite.source, 1, lines);
75
+ }
69
76
  lines.push("tests:");
70
77
 
71
78
  for (const test of suite.tests) {
72
79
  lines.push(` - name: ${yamlScalar(test.name)}`);
73
80
 
81
+ if (test.source && Object.keys(test.source).length > 0) {
82
+ lines.push(" source:");
83
+ serializeValue(test.source, 3, lines);
84
+ }
85
+
74
86
  // Write method-as-key (the shorthand)
75
87
  for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE"]) {
76
88
  if (method in test) {
@@ -92,6 +104,27 @@ export function serializeSuite(suite: RawSuite): string {
92
104
  serializeValue(test.json, 3, lines);
93
105
  }
94
106
 
107
+ // ARV-149: form body (application/x-www-form-urlencoded). The runner
108
+ // serialises this via URLSearchParams; values are flat strings with
109
+ // bracket notation for nested fields (e.g. `address[line1]`).
110
+ //
111
+ // ARV-162 (round-08 F19): form values are ALWAYS strings on the wire —
112
+ // x-www-form-urlencoded has no native numbers/bools/nulls. YAML parsing
113
+ // `phone: +1234567890` or `width: 12.5` as int/float makes `zond check
114
+ // tests` reject the suite ("expected string, received number"), and
115
+ // `zond run` silently skipped 21/68 generated Stripe suites this way.
116
+ // Force-quote every value regardless of shape; key still uses yamlScalar
117
+ // because bracket keys (`address[line1]`) need quoting too.
118
+ if (test.form !== undefined && typeof test.form === "object" && test.form !== null) {
119
+ const formEntries = Object.entries(test.form as Record<string, unknown>);
120
+ if (formEntries.length > 0) {
121
+ lines.push(" form:");
122
+ for (const [fk, fv] of formEntries) {
123
+ lines.push(` ${yamlScalar(fk)}: "${escapeYamlDoubleQuoted(String(fv))}"`);
124
+ }
125
+ }
126
+ }
127
+
95
128
  // query
96
129
  if (test.query) {
97
130
  lines.push(" query:");
@@ -103,6 +136,11 @@ export function serializeSuite(suite: RawSuite): string {
103
136
  lines.push(` skip_if: ${yamlScalar(String(test.skip_if))}`);
104
137
  }
105
138
 
139
+ // always (cleanup steps that survive cascade-skip on tainted captures)
140
+ if (test.always === true) {
141
+ lines.push(" always: true");
142
+ }
143
+
106
144
  // retry_until
107
145
  if (test.retry_until && typeof test.retry_until === "object") {
108
146
  const rt = test.retry_until as Record<string, unknown>;
@@ -131,7 +169,11 @@ export function serializeSuite(suite: RawSuite): string {
131
169
  if (hasExpect) {
132
170
  lines.push(" expect:");
133
171
  if (test.expect.status !== undefined) {
134
- lines.push(` status: ${test.expect.status}`);
172
+ if (Array.isArray(test.expect.status)) {
173
+ lines.push(` status: [${test.expect.status.join(", ")}]`);
174
+ } else {
175
+ lines.push(` status: ${test.expect.status}`);
176
+ }
135
177
  }
136
178
  if (test.expect.body) {
137
179
  lines.push(" body:");
@@ -142,7 +184,10 @@ export function serializeSuite(suite: RawSuite): string {
142
184
  lines.push(` ${rk}:`);
143
185
  serializeValue(rv, 6, lines);
144
186
  } else {
145
- lines.push(` ${rk}: ${yamlScalar(String(rv))}`);
187
+ // Preserve scalar types — `not_equals: true` (boolean) must not
188
+ // become `not_equals: "true"` (string), or assertions silently
189
+ // mistype against real boolean fields.
190
+ lines.push(` ${rk}: ${formatInlineValue(rv)}`);
146
191
  }
147
192
  }
148
193
  }
@@ -173,16 +218,24 @@ function serializeValue(value: unknown, indent: number, lines: string[]): void {
173
218
  if (entries.length > 0) {
174
219
  const [firstKey, firstVal] = entries[0]!;
175
220
  if (typeof firstVal === "object" && firstVal !== null) {
176
- lines.push(`${prefix}- ${firstKey}:`);
177
- serializeValue(firstVal, indent + 1, lines);
221
+ if (isEmptyContainer(firstVal)) {
222
+ lines.push(`${prefix}- ${firstKey}: ${Array.isArray(firstVal) ? "[]" : "{}"}`);
223
+ } else {
224
+ lines.push(`${prefix}- ${firstKey}:`);
225
+ serializeValue(firstVal, indent + 1, lines);
226
+ }
178
227
  } else {
179
228
  lines.push(`${prefix}- ${firstKey}: ${formatInlineValue(firstVal)}`);
180
229
  }
181
230
  for (let i = 1; i < entries.length; i++) {
182
231
  const [k, v] = entries[i]!;
183
232
  if (typeof v === "object" && v !== null) {
184
- lines.push(`${prefix} ${k}:`);
185
- serializeValue(v, indent + 1, lines);
233
+ if (isEmptyContainer(v)) {
234
+ lines.push(`${prefix} ${k}: ${Array.isArray(v) ? "[]" : "{}"}`);
235
+ } else {
236
+ lines.push(`${prefix} ${k}:`);
237
+ serializeValue(v, indent + 1, lines);
238
+ }
186
239
  } else {
187
240
  lines.push(`${prefix} ${k}: ${formatInlineValue(v)}`);
188
241
  }
@@ -200,8 +253,12 @@ function serializeValue(value: unknown, indent: number, lines: string[]): void {
200
253
  if (typeof value === "object") {
201
254
  for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
202
255
  if (typeof val === "object" && val !== null) {
203
- lines.push(`${prefix}${key}:`);
204
- serializeValue(val, indent + 1, lines);
256
+ if (isEmptyContainer(val)) {
257
+ lines.push(`${prefix}${key}: ${Array.isArray(val) ? "[]" : "{}"}`);
258
+ } else {
259
+ lines.push(`${prefix}${key}:`);
260
+ serializeValue(val, indent + 1, lines);
261
+ }
205
262
  } else {
206
263
  lines.push(`${prefix}${key}: ${formatInlineValue(val)}`);
207
264
  }
@@ -209,12 +266,32 @@ function serializeValue(value: unknown, indent: number, lines: string[]): void {
209
266
  }
210
267
  }
211
268
 
269
+ /** Empty `{}` / `[]` written as a bare `key:` (no value, no children) is
270
+ * re-parsed by YAML as `null` — sending `null` for an `object`-typed field
271
+ * guarantees 422 against strict APIs. Emit inline flow form to preserve type. */
272
+ function isEmptyContainer(val: unknown): boolean {
273
+ if (Array.isArray(val)) return val.length === 0;
274
+ if (typeof val === "object" && val !== null) return Object.keys(val).length === 0;
275
+ return false;
276
+ }
277
+
212
278
  function formatInlineValue(val: unknown): string {
213
279
  if (val === null || val === undefined) return "null";
214
280
  if (typeof val === "string") return yamlScalar(val);
215
281
  return String(val);
216
282
  }
217
283
 
284
+ /** ARV-62 (feedback round-01 / F3): security probes emit attack payloads
285
+ * with raw CRLF (`crlf:`, header-injection) and other control bytes.
286
+ * When written into a double-quoted YAML scalar these *must* be escaped
287
+ * (`\r` / `\n` / `\t` / `\xNN`) — emitting the raw byte produces YAML
288
+ * that the parser rejects with "bad indentation of a mapping entry"
289
+ * (`zond run` then fails the whole suite at load-time before sending a
290
+ * single request). The check also covers `\r`, `\t`, `\x00–\x1f`, and
291
+ * `\x7f` so any other control byte that sneaks into a payload survives
292
+ * the YAML round-trip. */
293
+ // eslint-disable-next-line no-control-regex
294
+ const CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
218
295
  function yamlScalar(value: string): string {
219
296
  if (
220
297
  value === "" ||
@@ -223,7 +300,6 @@ function yamlScalar(value: string): string {
223
300
  value === "null" ||
224
301
  value.includes(":") ||
225
302
  value.includes("#") ||
226
- value.includes("\n") ||
227
303
  value.includes("'") ||
228
304
  value.includes('"') ||
229
305
  value.includes("{") ||
@@ -236,9 +312,32 @@ function yamlScalar(value: string): string {
236
312
  value.startsWith("%") ||
237
313
  value.startsWith("@") ||
238
314
  value.startsWith("`") ||
239
- /^\d+$/.test(value)
315
+ /^\d+$/.test(value) ||
316
+ CONTROL_BYTE_RE.test(value)
240
317
  ) {
241
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
318
+ return `"${escapeYamlDoubleQuoted(value)}"`;
242
319
  }
243
320
  return value;
244
321
  }
322
+
323
+ function escapeYamlDoubleQuoted(value: string): string {
324
+ let out = "";
325
+ for (let i = 0; i < value.length; i++) {
326
+ const ch = value[i]!;
327
+ const code = ch.charCodeAt(0);
328
+ if (ch === "\\") { out += "\\\\"; continue; }
329
+ if (ch === '"') { out += '\\"'; continue; }
330
+ if (ch === "\n") { out += "\\n"; continue; }
331
+ if (ch === "\r") { out += "\\r"; continue; }
332
+ if (ch === "\t") { out += "\\t"; continue; }
333
+ if (code < 0x20 || code === 0x7f) {
334
+ // YAML 1.2 allows \xNN for any byte in (0..0xff); use this for any
335
+ // control character that doesn't have a dedicated short escape
336
+ // (covers \x00–\x1f minus the three handled above, plus DEL).
337
+ out += "\\x" + code.toString(16).padStart(2, "0");
338
+ continue;
339
+ }
340
+ out += ch;
341
+ }
342
+ return out;
343
+ }