@kirrosh/zond 0.22.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 +648 -0
  2. package/README.md +58 -6
  3. package/package.json +9 -6
  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 +43 -0
  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 +16 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +178 -7
  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 -46
  31. package/src/cli/commands/init/bootstrap.ts +30 -1
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +56 -3
  34. package/src/cli/commands/init/templates/agents.md +65 -61
  35. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  36. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  37. package/src/cli/commands/init/templates/skills/zond.md +592 -125
  38. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  39. package/src/cli/commands/prepare-fixtures.ts +135 -0
  40. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  41. package/src/cli/commands/probe/security.ts +454 -0
  42. package/src/cli/commands/probe/static.ts +255 -0
  43. package/src/cli/commands/probe/webhooks.ts +161 -0
  44. package/src/cli/commands/probe.ts +459 -0
  45. package/src/cli/commands/reference.ts +87 -0
  46. package/src/cli/commands/refresh-api.ts +169 -0
  47. package/src/cli/commands/remove-api.ts +150 -0
  48. package/src/cli/commands/report-bundle.ts +318 -0
  49. package/src/cli/commands/report.ts +241 -0
  50. package/src/cli/commands/request.ts +379 -4
  51. package/src/cli/commands/run.ts +842 -53
  52. package/src/cli/commands/session.ts +244 -0
  53. package/src/cli/commands/use.ts +18 -1
  54. package/src/cli/index.ts +20 -3
  55. package/src/cli/json-envelope.ts +112 -3
  56. package/src/cli/json-schemas.ts +263 -0
  57. package/src/cli/program.ts +198 -635
  58. package/src/cli/resolve.ts +105 -0
  59. package/src/cli/status-filter.ts +124 -0
  60. package/src/cli/util/api-context.ts +85 -0
  61. package/src/cli/version.ts +5 -0
  62. package/src/core/anti-fp/bootstrap.ts +34 -0
  63. package/src/core/anti-fp/index.ts +33 -0
  64. package/src/core/anti-fp/registry.ts +44 -0
  65. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  66. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  67. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  68. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  69. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  70. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  71. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  72. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  73. package/src/core/anti-fp/types.ts +68 -0
  74. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  75. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  76. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  77. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  78. package/src/core/checks/checks/cross_call_references.ts +134 -0
  79. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  80. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  81. package/src/core/checks/checks/ignored_auth.ts +211 -0
  82. package/src/core/checks/checks/index.ts +65 -0
  83. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  84. package/src/core/checks/checks/missing_required_header.ts +40 -0
  85. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  86. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  87. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  88. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  89. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  90. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  91. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  92. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  93. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  94. package/src/core/checks/checks/unsupported_method.ts +63 -0
  95. package/src/core/checks/checks/use_after_free.ts +78 -0
  96. package/src/core/checks/index.ts +30 -0
  97. package/src/core/checks/mode.ts +79 -0
  98. package/src/core/checks/recommended-action.ts +64 -0
  99. package/src/core/checks/registry.ts +78 -0
  100. package/src/core/checks/runner.ts +874 -0
  101. package/src/core/checks/sarif.ts +230 -0
  102. package/src/core/checks/stateful.ts +121 -0
  103. package/src/core/checks/types.ts +189 -0
  104. package/src/core/classifier/recommended-action.ts +222 -0
  105. package/src/core/context/current.ts +22 -6
  106. package/src/core/context/session.ts +78 -0
  107. package/src/core/coverage/loader.ts +185 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +151 -11
  110. package/src/core/diagnostics/failure-class.ts +120 -0
  111. package/src/core/diagnostics/failure-hints.ts +212 -9
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  114. package/src/core/exporter/case-study/index.ts +270 -0
  115. package/src/core/exporter/curl.ts +40 -0
  116. package/src/core/exporter/exporter.ts +48 -0
  117. package/src/core/exporter/html-report/escape.ts +24 -0
  118. package/src/core/exporter/html-report/index.ts +479 -0
  119. package/src/core/exporter/html-report/script.ts +100 -0
  120. package/src/core/exporter/html-report/styles.ts +408 -0
  121. package/src/core/generator/chunker.ts +42 -16
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/create-body.ts +89 -0
  124. package/src/core/generator/data-factory.ts +445 -19
  125. package/src/core/generator/describe.ts +1 -1
  126. package/src/core/generator/fixtures-builder.ts +325 -0
  127. package/src/core/generator/index.ts +7 -5
  128. package/src/core/generator/openapi-reader.ts +37 -3
  129. package/src/core/generator/path-param-disambig.ts +114 -0
  130. package/src/core/generator/resources-builder.ts +648 -0
  131. package/src/core/generator/schema-utils.ts +11 -3
  132. package/src/core/generator/serializer.ts +103 -13
  133. package/src/core/generator/suite-generator.ts +419 -111
  134. package/src/core/generator/types.ts +8 -0
  135. package/src/core/identity/identity-file.ts +129 -0
  136. package/src/core/lint/affects.ts +28 -0
  137. package/src/core/lint/config.ts +96 -0
  138. package/src/core/lint/format.ts +42 -0
  139. package/src/core/lint/index.ts +94 -0
  140. package/src/core/lint/reporter.ts +128 -0
  141. package/src/core/lint/rules/consistency.ts +158 -0
  142. package/src/core/lint/rules/heuristics.ts +97 -0
  143. package/src/core/lint/rules/strictness.ts +109 -0
  144. package/src/core/lint/types.ts +96 -0
  145. package/src/core/lint/walker.ts +248 -0
  146. package/src/core/meta/meta-store.ts +6 -73
  147. package/src/core/output/README.md +91 -0
  148. package/src/core/output/index.ts +13 -0
  149. package/src/core/output/run.ts +126 -0
  150. package/src/core/output/types.ts +129 -0
  151. package/src/core/parser/env-interpolation.ts +104 -0
  152. package/src/core/parser/filter.ts +57 -0
  153. package/src/core/parser/schema.ts +129 -4
  154. package/src/core/parser/types.ts +19 -1
  155. package/src/core/parser/variables.ts +0 -0
  156. package/src/core/parser/yaml-parser.ts +58 -12
  157. package/src/core/probe/bootstrap.ts +34 -0
  158. package/src/core/probe/dry-run-envelope.ts +57 -0
  159. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  160. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  161. package/src/core/probe/mass-assignment-template.ts +212 -0
  162. package/src/core/probe/method-probe.ts +43 -76
  163. package/src/core/probe/method-shared.ts +69 -0
  164. package/src/core/probe/negative-probe.ts +183 -149
  165. package/src/core/probe/orphan-tracker.ts +188 -0
  166. package/src/core/probe/path-discovery.ts +440 -0
  167. package/src/core/probe/probe-harness.ts +120 -0
  168. package/src/core/probe/registry.ts +89 -0
  169. package/src/core/probe/runner.ts +136 -0
  170. package/src/core/probe/security-probe-class.ts +201 -0
  171. package/src/core/probe/security-probe.ts +1453 -0
  172. package/src/core/probe/shared.ts +505 -0
  173. package/src/core/probe/static-probe-class.ts +125 -0
  174. package/src/core/probe/types.ts +165 -0
  175. package/src/core/probe/verdict-aggregator.ts +33 -0
  176. package/src/core/probe/webhooks-probe.ts +284 -0
  177. package/src/core/reporter/console.ts +41 -2
  178. package/src/core/reporter/index.ts +2 -3
  179. package/src/core/reporter/json.ts +11 -1
  180. package/src/core/reporter/junit.ts +27 -12
  181. package/src/core/reporter/ndjson.ts +37 -0
  182. package/src/core/reporter/types.ts +3 -0
  183. package/src/core/runner/assertions.ts +58 -1
  184. package/src/core/runner/async-pool.ts +108 -0
  185. package/src/core/runner/auth-path.ts +8 -0
  186. package/src/core/runner/ci-context.ts +72 -0
  187. package/src/core/runner/executor.ts +264 -20
  188. package/src/core/runner/form-encode.ts +51 -0
  189. package/src/core/runner/http-client.ts +75 -2
  190. package/src/core/runner/learn-drift.ts +293 -0
  191. package/src/core/runner/preflight-vars.ts +149 -0
  192. package/src/core/runner/progress-tracker.ts +73 -0
  193. package/src/core/runner/rate-limiter.ts +89 -17
  194. package/src/core/runner/run-kind.ts +39 -0
  195. package/src/core/runner/schema-validator.ts +312 -0
  196. package/src/core/runner/send-request.ts +153 -20
  197. package/src/core/runner/types.ts +38 -0
  198. package/src/core/secrets/registry.ts +164 -0
  199. package/src/core/secrets/secrets-file.ts +115 -0
  200. package/src/core/selectors/operation-filter.ts +144 -0
  201. package/src/core/setup-api.ts +415 -16
  202. package/src/core/severity/category.ts +94 -0
  203. package/src/core/severity/index.ts +121 -0
  204. package/src/core/spec/layers.ts +154 -0
  205. package/src/core/util/format-eta.ts +21 -0
  206. package/src/core/utils.ts +5 -1
  207. package/src/core/workspace/config.ts +129 -0
  208. package/src/core/workspace/manifest.ts +283 -0
  209. package/src/core/workspace/output-rotation.ts +62 -0
  210. package/src/core/workspace/triage-path.ts +87 -0
  211. package/src/db/lint-runs.ts +47 -0
  212. package/src/db/migrate.ts +126 -0
  213. package/src/db/migrations/0001_run_kind.sql +25 -0
  214. package/src/db/migrations/sql.d.ts +4 -0
  215. package/src/db/queries/collections.ts +133 -0
  216. package/src/db/queries/coverage.ts +9 -0
  217. package/src/db/queries/dashboard.ts +59 -0
  218. package/src/db/queries/results.ts +128 -0
  219. package/src/db/queries/runs.ts +235 -0
  220. package/src/db/queries/sessions.ts +42 -0
  221. package/src/db/queries/settings.ts +28 -0
  222. package/src/db/queries/types.ts +172 -0
  223. package/src/db/queries.ts +72 -802
  224. package/src/db/schema.ts +178 -50
  225. package/src/cli/commands/export.ts +0 -144
  226. package/src/cli/commands/guide.ts +0 -127
  227. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  228. package/src/cli/commands/probe-methods.ts +0 -108
  229. package/src/cli/commands/probe-validation.ts +0 -124
  230. package/src/cli/commands/serve.ts +0 -114
  231. package/src/cli/commands/sync.ts +0 -268
  232. package/src/cli/commands/update.ts +0 -189
  233. package/src/cli/commands/validate.ts +0 -34
  234. package/src/core/diagnostics/render-md.ts +0 -112
  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 -19
  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,6 +26,7 @@ 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
32
  status?: number | number[];
@@ -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:");
@@ -151,7 +184,10 @@ export function serializeSuite(suite: RawSuite): string {
151
184
  lines.push(` ${rk}:`);
152
185
  serializeValue(rv, 6, lines);
153
186
  } else {
154
- 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)}`);
155
191
  }
156
192
  }
157
193
  }
@@ -182,16 +218,24 @@ function serializeValue(value: unknown, indent: number, lines: string[]): void {
182
218
  if (entries.length > 0) {
183
219
  const [firstKey, firstVal] = entries[0]!;
184
220
  if (typeof firstVal === "object" && firstVal !== null) {
185
- lines.push(`${prefix}- ${firstKey}:`);
186
- 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
+ }
187
227
  } else {
188
228
  lines.push(`${prefix}- ${firstKey}: ${formatInlineValue(firstVal)}`);
189
229
  }
190
230
  for (let i = 1; i < entries.length; i++) {
191
231
  const [k, v] = entries[i]!;
192
232
  if (typeof v === "object" && v !== null) {
193
- lines.push(`${prefix} ${k}:`);
194
- 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
+ }
195
239
  } else {
196
240
  lines.push(`${prefix} ${k}: ${formatInlineValue(v)}`);
197
241
  }
@@ -209,8 +253,12 @@ function serializeValue(value: unknown, indent: number, lines: string[]): void {
209
253
  if (typeof value === "object") {
210
254
  for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
211
255
  if (typeof val === "object" && val !== null) {
212
- lines.push(`${prefix}${key}:`);
213
- 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
+ }
214
262
  } else {
215
263
  lines.push(`${prefix}${key}: ${formatInlineValue(val)}`);
216
264
  }
@@ -218,12 +266,32 @@ function serializeValue(value: unknown, indent: number, lines: string[]): void {
218
266
  }
219
267
  }
220
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
+
221
278
  function formatInlineValue(val: unknown): string {
222
279
  if (val === null || val === undefined) return "null";
223
280
  if (typeof val === "string") return yamlScalar(val);
224
281
  return String(val);
225
282
  }
226
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]/;
227
295
  function yamlScalar(value: string): string {
228
296
  if (
229
297
  value === "" ||
@@ -232,7 +300,6 @@ function yamlScalar(value: string): string {
232
300
  value === "null" ||
233
301
  value.includes(":") ||
234
302
  value.includes("#") ||
235
- value.includes("\n") ||
236
303
  value.includes("'") ||
237
304
  value.includes('"') ||
238
305
  value.includes("{") ||
@@ -245,9 +312,32 @@ function yamlScalar(value: string): string {
245
312
  value.startsWith("%") ||
246
313
  value.startsWith("@") ||
247
314
  value.startsWith("`") ||
248
- /^\d+$/.test(value)
315
+ /^\d+$/.test(value) ||
316
+ CONTROL_BYTE_RE.test(value)
249
317
  ) {
250
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
318
+ return `"${escapeYamlDoubleQuoted(value)}"`;
251
319
  }
252
320
  return value;
253
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
+ }