@kirrosh/zond 0.26.0 → 0.27.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 (261) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -21
  3. package/bin/zond.mjs +23 -0
  4. package/package.json +13 -16
  5. package/scripts/npm/postinstall.mjs +76 -0
  6. package/src/CLAUDE.md +0 -112
  7. package/src/bun-types.d.ts +0 -5
  8. package/src/cli/argv.ts +0 -122
  9. package/src/cli/commands/add-api.ts +0 -146
  10. package/src/cli/commands/api/annotate/idempotency.ts +0 -59
  11. package/src/cli/commands/api/annotate/index.ts +0 -880
  12. package/src/cli/commands/api/annotate/lifecycle.ts +0 -74
  13. package/src/cli/commands/api/annotate/overlay.ts +0 -206
  14. package/src/cli/commands/api/annotate/pagination.ts +0 -64
  15. package/src/cli/commands/api/annotate/prompts.ts +0 -220
  16. package/src/cli/commands/api/annotate/readback.ts +0 -58
  17. package/src/cli/commands/api/annotate/resources.ts +0 -91
  18. package/src/cli/commands/api/annotate/seed-bodies.ts +0 -61
  19. package/src/cli/commands/audit.ts +0 -786
  20. package/src/cli/commands/catalog.ts +0 -97
  21. package/src/cli/commands/check.ts +0 -361
  22. package/src/cli/commands/checks.ts +0 -1072
  23. package/src/cli/commands/ci-init.ts +0 -223
  24. package/src/cli/commands/clean.ts +0 -212
  25. package/src/cli/commands/cleanup.ts +0 -236
  26. package/src/cli/commands/completions.ts +0 -192
  27. package/src/cli/commands/coverage.ts +0 -872
  28. package/src/cli/commands/db.ts +0 -611
  29. package/src/cli/commands/describe.ts +0 -120
  30. package/src/cli/commands/discover.ts +0 -1356
  31. package/src/cli/commands/doctor.ts +0 -661
  32. package/src/cli/commands/fixtures.ts +0 -402
  33. package/src/cli/commands/generate.ts +0 -577
  34. package/src/cli/commands/init/agents-md.ts +0 -61
  35. package/src/cli/commands/init/bootstrap.ts +0 -111
  36. package/src/cli/commands/init/index.ts +0 -244
  37. package/src/cli/commands/init/skills.ts +0 -141
  38. package/src/cli/commands/init/templates/agents.md +0 -86
  39. package/src/cli/commands/init/templates/markdown.d.ts +0 -4
  40. package/src/cli/commands/init/templates/skills/warm-up-target.md +0 -122
  41. package/src/cli/commands/init/templates/skills/zond-checks.md +0 -621
  42. package/src/cli/commands/init/templates/skills/zond-seed.md +0 -114
  43. package/src/cli/commands/init/templates/skills/zond-triage.md +0 -272
  44. package/src/cli/commands/init/templates/skills/zond.md +0 -861
  45. package/src/cli/commands/init/templates/zond-config.yml +0 -14
  46. package/src/cli/commands/prepare-fixtures.ts +0 -97
  47. package/src/cli/commands/probe/_seed-bodies.ts +0 -52
  48. package/src/cli/commands/probe/mass-assignment.ts +0 -594
  49. package/src/cli/commands/probe/security.ts +0 -537
  50. package/src/cli/commands/probe/static.ts +0 -255
  51. package/src/cli/commands/probe/webhooks.ts +0 -163
  52. package/src/cli/commands/probe.ts +0 -535
  53. package/src/cli/commands/reference.ts +0 -87
  54. package/src/cli/commands/refresh-api.ts +0 -227
  55. package/src/cli/commands/remove-api.ts +0 -150
  56. package/src/cli/commands/report-bundle.ts +0 -310
  57. package/src/cli/commands/report.ts +0 -241
  58. package/src/cli/commands/request.ts +0 -548
  59. package/src/cli/commands/run.ts +0 -1142
  60. package/src/cli/commands/schema-from-runs.ts +0 -128
  61. package/src/cli/commands/secrets.ts +0 -133
  62. package/src/cli/commands/session.ts +0 -244
  63. package/src/cli/commands/use.ts +0 -74
  64. package/src/cli/index.ts +0 -55
  65. package/src/cli/json-envelope.ts +0 -108
  66. package/src/cli/json-schemas.ts +0 -314
  67. package/src/cli/output.ts +0 -40
  68. package/src/cli/program.ts +0 -219
  69. package/src/cli/resolve.ts +0 -105
  70. package/src/cli/runtime.ts +0 -7
  71. package/src/cli/safe-live.ts +0 -24
  72. package/src/cli/status-filter.ts +0 -114
  73. package/src/cli/util/api-context.ts +0 -85
  74. package/src/cli/version.ts +0 -8
  75. package/src/core/audit/persist.ts +0 -183
  76. package/src/core/checks/budget.ts +0 -59
  77. package/src/core/checks/checks/_crud-helpers.ts +0 -133
  78. package/src/core/checks/checks/_negative_mutator.ts +0 -133
  79. package/src/core/checks/checks/_readback-helpers.ts +0 -133
  80. package/src/core/checks/checks/content_type_conformance.ts +0 -39
  81. package/src/core/checks/checks/cross_call_references.ts +0 -147
  82. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +0 -219
  83. package/src/core/checks/checks/ensure_resource_availability.ts +0 -62
  84. package/src/core/checks/checks/idempotency_replay.ts +0 -242
  85. package/src/core/checks/checks/ignored_auth.ts +0 -254
  86. package/src/core/checks/checks/index.ts +0 -68
  87. package/src/core/checks/checks/lifecycle_transitions.ts +0 -416
  88. package/src/core/checks/checks/missing_required_header.ts +0 -40
  89. package/src/core/checks/checks/negative_data_rejection.ts +0 -148
  90. package/src/core/checks/checks/not_a_server_error.ts +0 -35
  91. package/src/core/checks/checks/open_cors_on_sensitive.ts +0 -160
  92. package/src/core/checks/checks/pagination_invariants.ts +0 -419
  93. package/src/core/checks/checks/positive_data_acceptance.ts +0 -33
  94. package/src/core/checks/checks/rate_limit_headers_absent.ts +0 -77
  95. package/src/core/checks/checks/response_headers_conformance.ts +0 -74
  96. package/src/core/checks/checks/response_schema_conformance.ts +0 -30
  97. package/src/core/checks/checks/status_code_conformance.ts +0 -132
  98. package/src/core/checks/checks/unsupported_method.ts +0 -63
  99. package/src/core/checks/checks/use_after_free.ts +0 -78
  100. package/src/core/checks/index.ts +0 -30
  101. package/src/core/checks/mode.ts +0 -82
  102. package/src/core/checks/recommended-action.ts +0 -68
  103. package/src/core/checks/registry.ts +0 -78
  104. package/src/core/checks/runner.ts +0 -1461
  105. package/src/core/checks/sarif.ts +0 -230
  106. package/src/core/checks/spec-findings.ts +0 -308
  107. package/src/core/checks/stateful.ts +0 -121
  108. package/src/core/checks/types.ts +0 -305
  109. package/src/core/checks/zond-extensions.ts +0 -73
  110. package/src/core/classifier/recommended-action.ts +0 -251
  111. package/src/core/context/current.ts +0 -51
  112. package/src/core/context/session.ts +0 -78
  113. package/src/core/coverage/loader.ts +0 -216
  114. package/src/core/coverage/reasons.ts +0 -300
  115. package/src/core/diagnostics/db-analysis.ts +0 -666
  116. package/src/core/diagnostics/failure-class.ts +0 -140
  117. package/src/core/diagnostics/failure-hints.ts +0 -112
  118. package/src/core/diagnostics/spec-pointer.ts +0 -99
  119. package/src/core/diagnostics/suggested-fixes.ts +0 -155
  120. package/src/core/exporter/case-study/index.ts +0 -270
  121. package/src/core/exporter/curl.ts +0 -40
  122. package/src/core/exporter/exporter.ts +0 -48
  123. package/src/core/exporter/html-report/escape.ts +0 -24
  124. package/src/core/exporter/html-report/index.ts +0 -479
  125. package/src/core/exporter/html-report/script.ts +0 -100
  126. package/src/core/exporter/html-report/styles.ts +0 -408
  127. package/src/core/generator/catalog-builder.ts +0 -179
  128. package/src/core/generator/chunker.ts +0 -78
  129. package/src/core/generator/coverage-phase.ts +0 -0
  130. package/src/core/generator/coverage-scanner.ts +0 -87
  131. package/src/core/generator/data-factory.ts +0 -759
  132. package/src/core/generator/describe.ts +0 -302
  133. package/src/core/generator/endpoint-warnings.ts +0 -52
  134. package/src/core/generator/fixtures-builder.ts +0 -332
  135. package/src/core/generator/index.ts +0 -14
  136. package/src/core/generator/openapi-reader.ts +0 -297
  137. package/src/core/generator/path-param-disambig.ts +0 -140
  138. package/src/core/generator/resources-builder.ts +0 -898
  139. package/src/core/generator/schema-utils.ts +0 -112
  140. package/src/core/generator/serializer.ts +0 -343
  141. package/src/core/generator/suite-generator.ts +0 -1301
  142. package/src/core/generator/types.ts +0 -63
  143. package/src/core/identity/identity-file.ts +0 -0
  144. package/src/core/lint/affects.ts +0 -28
  145. package/src/core/lint/config.ts +0 -96
  146. package/src/core/lint/format.ts +0 -42
  147. package/src/core/lint/index.ts +0 -94
  148. package/src/core/lint/reporter.ts +0 -128
  149. package/src/core/lint/rules/consistency.ts +0 -158
  150. package/src/core/lint/rules/heuristics.ts +0 -97
  151. package/src/core/lint/rules/strictness.ts +0 -109
  152. package/src/core/lint/types.ts +0 -96
  153. package/src/core/lint/walker.ts +0 -248
  154. package/src/core/meta/meta-store.ts +0 -11
  155. package/src/core/output/README.md +0 -73
  156. package/src/core/output/index.ts +0 -13
  157. package/src/core/output/run.ts +0 -91
  158. package/src/core/output/types.ts +0 -122
  159. package/src/core/parser/dynamic-values.ts +0 -160
  160. package/src/core/parser/env-interpolation.ts +0 -104
  161. package/src/core/parser/filter.ts +0 -97
  162. package/src/core/parser/schema.ts +0 -359
  163. package/src/core/parser/types.ts +0 -117
  164. package/src/core/parser/variables.ts +0 -0
  165. package/src/core/parser/yaml-parser.ts +0 -181
  166. package/src/core/probe/bootstrap.ts +0 -34
  167. package/src/core/probe/dry-run-envelope.ts +0 -61
  168. package/src/core/probe/mass-assignment/classify.ts +0 -175
  169. package/src/core/probe/mass-assignment/cleanup.ts +0 -52
  170. package/src/core/probe/mass-assignment/digest.ts +0 -114
  171. package/src/core/probe/mass-assignment/orchestrator.ts +0 -459
  172. package/src/core/probe/mass-assignment/regression.ts +0 -141
  173. package/src/core/probe/mass-assignment/suspects.ts +0 -92
  174. package/src/core/probe/mass-assignment/types.ts +0 -135
  175. package/src/core/probe/mass-assignment-probe-class.ts +0 -198
  176. package/src/core/probe/mass-assignment-probe.ts +0 -27
  177. package/src/core/probe/mass-assignment-template.ts +0 -240
  178. package/src/core/probe/method-probe.ts +0 -164
  179. package/src/core/probe/method-shared.ts +0 -69
  180. package/src/core/probe/negative-probe.ts +0 -691
  181. package/src/core/probe/orphan-tracker.ts +0 -188
  182. package/src/core/probe/path-discovery.ts +0 -439
  183. package/src/core/probe/probe-harness.ts +0 -119
  184. package/src/core/probe/registry.ts +0 -89
  185. package/src/core/probe/runner.ts +0 -136
  186. package/src/core/probe/security/baseline.ts +0 -174
  187. package/src/core/probe/security/classify.ts +0 -341
  188. package/src/core/probe/security/cleanup.ts +0 -125
  189. package/src/core/probe/security/detectors.ts +0 -71
  190. package/src/core/probe/security/digest.ts +0 -104
  191. package/src/core/probe/security/orchestrator.ts +0 -398
  192. package/src/core/probe/security/regression.ts +0 -103
  193. package/src/core/probe/security/types.ts +0 -151
  194. package/src/core/probe/security-probe-class.ts +0 -207
  195. package/src/core/probe/security-probe.ts +0 -32
  196. package/src/core/probe/shared.ts +0 -531
  197. package/src/core/probe/static-probe-class.ts +0 -125
  198. package/src/core/probe/types.ts +0 -165
  199. package/src/core/probe/verdict-aggregator.ts +0 -33
  200. package/src/core/probe/webhooks-probe.ts +0 -282
  201. package/src/core/reporter/console.ts +0 -240
  202. package/src/core/reporter/index.ts +0 -22
  203. package/src/core/reporter/json.ts +0 -22
  204. package/src/core/reporter/junit.ts +0 -93
  205. package/src/core/reporter/ndjson.ts +0 -37
  206. package/src/core/reporter/types.ts +0 -15
  207. package/src/core/runner/assertions.ts +0 -383
  208. package/src/core/runner/async-pool.ts +0 -108
  209. package/src/core/runner/auth-path.ts +0 -8
  210. package/src/core/runner/ci-context.ts +0 -72
  211. package/src/core/runner/executor.ts +0 -652
  212. package/src/core/runner/expr-eval.ts +0 -41
  213. package/src/core/runner/form-encode.ts +0 -41
  214. package/src/core/runner/http-client.ts +0 -224
  215. package/src/core/runner/learn-drift.ts +0 -293
  216. package/src/core/runner/preflight-vars.ts +0 -153
  217. package/src/core/runner/progress-tracker.ts +0 -73
  218. package/src/core/runner/rate-limiter.ts +0 -185
  219. package/src/core/runner/run-kind.ts +0 -45
  220. package/src/core/runner/schema-validator.ts +0 -308
  221. package/src/core/runner/send-request.ts +0 -232
  222. package/src/core/runner/transforms.ts +0 -65
  223. package/src/core/runner/types.ts +0 -94
  224. package/src/core/secrets/registry.ts +0 -164
  225. package/src/core/secrets/secrets-file.ts +0 -115
  226. package/src/core/selectors/operation-filter.ts +0 -144
  227. package/src/core/setup-api.ts +0 -590
  228. package/src/core/severity/category.ts +0 -94
  229. package/src/core/severity/index.ts +0 -58
  230. package/src/core/spec/infer-schema.ts +0 -102
  231. package/src/core/spec/layers.ts +0 -154
  232. package/src/core/spec/merge-specs.ts +0 -156
  233. package/src/core/spec/schema-from-runs.ts +0 -117
  234. package/src/core/spec/schema-overlay.ts +0 -130
  235. package/src/core/util/ajv.ts +0 -13
  236. package/src/core/util/format-eta.ts +0 -21
  237. package/src/core/util/headers.ts +0 -9
  238. package/src/core/util/url.ts +0 -24
  239. package/src/core/utils.ts +0 -13
  240. package/src/core/workspace/config.ts +0 -129
  241. package/src/core/workspace/fixture-gap-report.ts +0 -84
  242. package/src/core/workspace/fixture-gaps.ts +0 -71
  243. package/src/core/workspace/manifest.ts +0 -283
  244. package/src/core/workspace/output-rotation.ts +0 -62
  245. package/src/core/workspace/root.ts +0 -96
  246. package/src/core/workspace/triage-path.ts +0 -87
  247. package/src/db/lint-runs.ts +0 -47
  248. package/src/db/migrate.ts +0 -128
  249. package/src/db/migrations/0001_run_kind.sql +0 -25
  250. package/src/db/migrations/0002_run_kind_request.sql +0 -59
  251. package/src/db/migrations/sql.d.ts +0 -4
  252. package/src/db/queries/collections.ts +0 -133
  253. package/src/db/queries/coverage.ts +0 -9
  254. package/src/db/queries/dashboard.ts +0 -59
  255. package/src/db/queries/results.ts +0 -216
  256. package/src/db/queries/runs.ts +0 -289
  257. package/src/db/queries/sessions.ts +0 -42
  258. package/src/db/queries/settings.ts +0 -28
  259. package/src/db/queries/types.ts +0 -172
  260. package/src/db/queries.ts +0 -75
  261. package/src/db/schema.ts +0 -298
@@ -1,308 +0,0 @@
1
- import Ajv from "ajv";
2
- import type { ErrorObject, ValidateFunction, AnySchema } from "ajv";
3
- import type { OpenAPIV3 } from "openapi-types";
4
- import { makeAjv } from "../util/ajv.ts";
5
- import { specPathToRegex } from "../generator/coverage-scanner.ts";
6
- import type { AssertionResult } from "./types.ts";
7
-
8
- export interface SchemaValidator {
9
- validate(method: string, path: string, status: number, body: unknown): AssertionResult[];
10
- /** TASK-142: surface whether an endpoint and a response branch matched.
11
- * Lets ad-hoc callers (`zond request --validate-schema`) distinguish
12
- * "no spec entry for this URL" from "spec entry exists, body is valid". */
13
- inspect(method: string, path: string, status: number): {
14
- matchedEndpoint: { method: string; path: string } | null;
15
- matchedResponseStatus: string | null;
16
- hasJsonSchema: boolean;
17
- };
18
- }
19
-
20
- interface EndpointEntry {
21
- method: string;
22
- path: string;
23
- regex: RegExp;
24
- responses: OpenAPIV3.ResponsesObject;
25
- }
26
-
27
- const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const;
28
-
29
- export function createSchemaValidator(doc: OpenAPIV3.Document): SchemaValidator {
30
- const isV31 = typeof doc.openapi === "string" && doc.openapi.startsWith("3.1");
31
- // OpenAPI 3.1 → JSON Schema Draft 2020-12; 3.0 → Draft 4/7-ish.
32
- // verbose:true exposes parentSchema on each error so humanize() can render
33
- // the full required-set alongside the missing field name (TASK-277).
34
- const ajv = makeAjv(isV31, { strict: false, allErrors: true, verbose: true });
35
- applyStrictFormats(ajv);
36
-
37
- const endpoints: EndpointEntry[] = [];
38
- if (doc.paths) {
39
- for (const [pathTpl, pathItem] of Object.entries(doc.paths)) {
40
- if (!pathItem) continue;
41
- const regex = specPathToRegex(pathTpl);
42
- for (const m of HTTP_METHODS) {
43
- const op = (pathItem as Record<string, unknown>)[m] as OpenAPIV3.OperationObject | undefined;
44
- if (!op || !op.responses) continue;
45
- endpoints.push({ method: m.toUpperCase(), path: pathTpl, regex, responses: op.responses });
46
- }
47
- }
48
- // Sort by specificity so concrete paths (e.g. /users/me) win over templated
49
- // ones (/users/{id}) regardless of spec declaration order. Tie-breaker is
50
- // stable insertion order (Array.sort is stable in modern engines).
51
- endpoints.sort((a, b) => paramCount(a.path) - paramCount(b.path));
52
- }
53
-
54
- // ARV-214: per-schema compile is the dominant cost of --validate-schema
55
- // on big dereferenced specs (github 14 MiB → minutes per response).
56
- // The cache below is keyed by schema *object reference*, so endpoints
57
- // that share a $ref source after dereference hit it on the second
58
- // access. The slow_compile budget warns the user the first time a
59
- // schema crosses 1s — the same compile only happens once per schema
60
- // anyway thanks to the cache, so the warning is per-source, not
61
- // per-call.
62
- const SLOW_COMPILE_MS = Number(process.env.ZOND_VALIDATE_SCHEMA_SLOW_COMPILE_MS ?? "1000");
63
- // Hard byte cap stops the run from hanging minutes on a pathological
64
- // single response schema (post-deref github Repository / kubernetes
65
- // pod-spec). Returning a no-op validator + a single warning is much
66
- // friendlier than blocking on `ajv.compile` for an unbounded time.
67
- // Default chosen from the bench in /tmp/bench-convert.ts: a 572 KiB
68
- // schema compiles in ~800 ms; 1 MiB ≈ 1.4 s; beyond that we'd rather
69
- // skip and tell the user.
70
- const MAX_SCHEMA_BYTES = Number(process.env.ZOND_VALIDATE_SCHEMA_MAX_BYTES ?? String(1_048_576));
71
- const compiled = new Map<unknown, ValidateFunction | null>();
72
- const warnedSlow = new WeakSet<object>();
73
- const warnedTooLarge = new WeakSet<object>();
74
-
75
- function tooLargeSentinel(): null {
76
- return null;
77
- }
78
-
79
- function compile(schema: AnySchema): ValidateFunction | null {
80
- if (compiled.has(schema)) return compiled.get(schema) ?? null;
81
- // Cheap pre-check: a schema whose JSON serialisation exceeds
82
- // MAX_SCHEMA_BYTES is almost certainly going to take seconds-to-
83
- // minutes to compile and produce noisy mid-body errors that aren't
84
- // worth the wait. Skip it with a warning. JSON.stringify itself is
85
- // O(n) but at MiB-scale completes in tens of ms — orders of
86
- // magnitude cheaper than ajv.compile on the same payload.
87
- if (MAX_SCHEMA_BYTES > 0) {
88
- try {
89
- const sz = JSON.stringify(schema)?.length ?? 0;
90
- if (sz > MAX_SCHEMA_BYTES) {
91
- if (typeof schema === "object" && schema && !warnedTooLarge.has(schema as object)) {
92
- warnedTooLarge.add(schema as object);
93
- const kib = Math.round(sz / 1024);
94
- process.stderr.write(
95
- `[zond] schema too large for --validate-schema (${kib} KiB > ${Math.round(MAX_SCHEMA_BYTES / 1024)} KiB) — skipping; raise via ZOND_VALIDATE_SCHEMA_MAX_BYTES.\n`,
96
- );
97
- }
98
- compiled.set(schema, tooLargeSentinel());
99
- return null;
100
- }
101
- } catch { /* circular or non-serialisable — let ajv try */ }
102
- }
103
- const prepared = isV31 ? schema : convertOpenApi30(schema);
104
- const t0 = performance.now();
105
- const fn = ajv.compile(prepared as AnySchema);
106
- const elapsed = performance.now() - t0;
107
- if (elapsed >= SLOW_COMPILE_MS && typeof schema === "object" && schema && !warnedSlow.has(schema as object)) {
108
- warnedSlow.add(schema as object);
109
- process.stderr.write(
110
- `[zond] schema validator compile took ${elapsed.toFixed(0)} ms (warn ≥ ${SLOW_COMPILE_MS} ms) — see ZOND_VALIDATE_SCHEMA_MAX_BYTES if --validate-schema runs feel slow.\n`,
111
- );
112
- }
113
- compiled.set(schema, fn);
114
- return fn;
115
- }
116
-
117
- function findResponseSchema(method: string, path: string, status: number): OpenAPIV3.SchemaObject | undefined {
118
- const upper = method.toUpperCase();
119
- // Endpoints are pre-sorted by specificity (concrete paths first), so the
120
- // first regex match is the most specific — /users/me beats /users/{id}.
121
- const match = endpoints.find(e => e.method === upper && e.regex.test(path));
122
- if (!match) return undefined;
123
- const responses = match.responses;
124
- const exact = responses[String(status)] as OpenAPIV3.ResponseObject | undefined;
125
- const wildcard = responses[`${Math.floor(status / 100)}XX`] as OpenAPIV3.ResponseObject | undefined;
126
- const fallback = responses.default as OpenAPIV3.ResponseObject | undefined;
127
- const response = exact ?? wildcard ?? fallback;
128
- if (!response || !response.content) return undefined;
129
- const json = response.content["application/json"];
130
- return (json?.schema as OpenAPIV3.SchemaObject | undefined) ?? undefined;
131
- }
132
-
133
- function inspectMatch(method: string, path: string, status: number) {
134
- const upper = method.toUpperCase();
135
- const ep = endpoints.find(e => e.method === upper && e.regex.test(path));
136
- if (!ep) {
137
- return { matchedEndpoint: null, matchedResponseStatus: null, hasJsonSchema: false };
138
- }
139
- const exact = ep.responses[String(status)] as OpenAPIV3.ResponseObject | undefined;
140
- const wildcard = ep.responses[`${Math.floor(status / 100)}XX`] as OpenAPIV3.ResponseObject | undefined;
141
- const fallback = ep.responses.default as OpenAPIV3.ResponseObject | undefined;
142
- const matchedKey = exact ? String(status) : wildcard ? `${Math.floor(status / 100)}XX` : fallback ? "default" : null;
143
- const response = exact ?? wildcard ?? fallback;
144
- const hasJsonSchema = !!(response?.content?.["application/json"]?.schema);
145
- return {
146
- matchedEndpoint: { method: ep.method, path: ep.path },
147
- matchedResponseStatus: matchedKey,
148
- hasJsonSchema,
149
- };
150
- }
151
-
152
- return {
153
- validate(method, path, status, body) {
154
- const schema = findResponseSchema(method, path, status);
155
- if (!schema) return [];
156
- let validator: ValidateFunction | null;
157
- try {
158
- validator = compile(schema);
159
- } catch (err) {
160
- return [{
161
- field: "body",
162
- rule: "schema.compile_error",
163
- passed: false,
164
- actual: undefined,
165
- expected: err instanceof Error ? err.message : String(err),
166
- }];
167
- }
168
- // ARV-214: schema crossed ZOND_VALIDATE_SCHEMA_MAX_BYTES — surface
169
- // the skip as a passing assertion so the run stays green and the
170
- // user knows validation was bypassed for this body.
171
- if (validator === null) {
172
- return [{
173
- field: "body",
174
- rule: "schema.skipped_too_large",
175
- passed: true,
176
- actual: undefined,
177
- expected: "schema exceeded ZOND_VALIDATE_SCHEMA_MAX_BYTES — see stderr warning",
178
- kind: "schema",
179
- }];
180
- }
181
- const ok = validator(body);
182
- if (ok) return [];
183
- const errors = validator.errors ?? [];
184
- return errors.map(e => ajvErrorToAssertion(e, body));
185
- },
186
- inspect: inspectMatch,
187
- };
188
- }
189
-
190
- function paramCount(path: string): number {
191
- let n = 0;
192
- for (const seg of path.split("/")) if (/^\{[^}]+\}$/.test(seg)) n++;
193
- return n;
194
- }
195
-
196
- function ajvErrorToAssertion(err: ErrorObject, body: unknown): AssertionResult {
197
- const ptr = err.instancePath || "";
198
- // Field key like "body" or "body.user.email" for parity with checkAssertions.
199
- const field = ptr ? `body${ptr.replace(/\//g, ".")}` : "body";
200
- const actual = ptr ? getByJsonPointer(body, ptr) : body;
201
- return {
202
- field,
203
- rule: `schema.${err.keyword}`,
204
- passed: false,
205
- actual,
206
- expected: humanize(err),
207
- kind: "schema",
208
- };
209
- }
210
-
211
- function humanize(err: ErrorObject): string {
212
- switch (err.keyword) {
213
- case "required": {
214
- const missing = (err.params as { missingProperty: string }).missingProperty;
215
- // verbose:true gives us the parent schema; surface the full required-set
216
- // so the user can see drift at a glance instead of decoding the message
217
- // one missing-field-error at a time (TASK-277).
218
- const parent = (err as ErrorObject & { parentSchema?: unknown }).parentSchema;
219
- const required =
220
- parent && typeof parent === "object" && Array.isArray((parent as { required?: unknown }).required)
221
- ? ((parent as { required: string[] }).required)
222
- : undefined;
223
- const tail = required ? `; expected required: [${required.join(", ")}]` : "";
224
- return `missing required field "${missing}"${tail}`;
225
- }
226
- case "type":
227
- return `type ${(err.params as { type: string | string[] }).type}`;
228
- case "enum":
229
- return `one of ${JSON.stringify((err.params as { allowedValues: unknown[] }).allowedValues)}`;
230
- case "format":
231
- return `format "${(err.params as { format: string }).format}"`;
232
- case "additionalProperties":
233
- return `no additional property "${(err.params as { additionalProperty: string }).additionalProperty}"`;
234
- case "const":
235
- return `const ${JSON.stringify((err.params as { allowedValue: unknown }).allowedValue)}`;
236
- case "minLength":
237
- case "maxLength":
238
- case "minimum":
239
- case "maximum":
240
- case "exclusiveMinimum":
241
- case "exclusiveMaximum":
242
- case "multipleOf":
243
- case "pattern":
244
- return `${err.keyword} ${JSON.stringify((err.params as Record<string, unknown>)[err.keyword] ?? "")}`.trim();
245
- default:
246
- return err.message ?? err.keyword;
247
- }
248
- }
249
-
250
- function getByJsonPointer(obj: unknown, pointer: string): unknown {
251
- if (!pointer) return obj;
252
- const segments = pointer.split("/").slice(1).map(s => s.replace(/~1/g, "/").replace(/~0/g, "~"));
253
- let cur: unknown = obj;
254
- for (const seg of segments) {
255
- if (cur && typeof cur === "object") {
256
- cur = (cur as Record<string, unknown>)[seg];
257
- } else {
258
- return undefined;
259
- }
260
- }
261
- return cur;
262
- }
263
-
264
- // RFC3339 §5.6: date-time = full-date "T" full-time. T (or t) is required as
265
- // separator; offset is "Z" or "[+-]HH:MM" with explicit colon. ajv-formats
266
- // accepts " " as separator and "+HH" without colon, which lets PostgreSQL-style
267
- // timestamps ("2026-04-29 07:10:44.674675+00") slip through.
268
- export const STRICT_RFC3339_DATE_TIME =
269
- /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])[Tt](?:[01]\d|2[0-3]):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
270
-
271
- function applyStrictFormats(ajv: Ajv): void {
272
- // Override ajv-formats' lax date-time with strict RFC3339.
273
- ajv.addFormat("date-time", { type: "string", validate: STRICT_RFC3339_DATE_TIME });
274
- }
275
-
276
- /**
277
- * Convert OpenAPI 3.0 schema to JSON Schema Draft 7-compatible:
278
- * - `nullable: true` → add "null" to `type`.
279
- * - Drop unsupported `example`, `xml`, `discriminator` keywords (ajv tolerates with strict:false).
280
- */
281
- function convertOpenApi30(schema: AnySchema): AnySchema {
282
- if (!schema || typeof schema !== "object") return schema;
283
- if (Array.isArray(schema)) {
284
- return schema.map(s => convertOpenApi30(s as AnySchema)) as unknown as AnySchema;
285
- }
286
- const src = schema as Record<string, unknown>;
287
- const out: Record<string, unknown> = {};
288
- for (const [k, v] of Object.entries(src)) {
289
- if (k === "nullable") continue;
290
- if (k === "type" && src.nullable === true) {
291
- if (Array.isArray(v)) {
292
- out.type = [...v as unknown[], "null"];
293
- } else if (typeof v === "string") {
294
- out.type = [v, "null"];
295
- } else {
296
- out.type = v;
297
- }
298
- continue;
299
- }
300
- if (v && typeof v === "object") {
301
- out[k] = convertOpenApi30(v as AnySchema);
302
- } else {
303
- out[k] = v;
304
- }
305
- }
306
- // Standalone nullable: true with no explicit type → leave as-is (ajv accepts any).
307
- return out as AnySchema;
308
- }
@@ -1,232 +0,0 @@
1
- import { executeRequest } from "./http-client.ts";
2
- import { loadEnvironment, substituteString, substituteDeep } from "../parser/variables.ts";
3
- import { getDb } from "../../db/schema.ts";
4
- import { findCollectionByNameOrId } from "../../db/queries.ts";
5
- import { encodeFormBody } from "./form-encode.ts";
6
- import { hasHeaderCI } from "../util/headers.ts";
7
-
8
- export function extractByPath(obj: unknown, path: string): unknown {
9
- return extractByPathWithDiagnostic(obj, path).value;
10
- }
11
-
12
- /** ARV-70 (feedback round-01 / F11): same extractor as above but also
13
- * reports *why* the path failed. The CLI surfaces this on stderr when
14
- * `--json-path` returns undefined so users don't lose minutes debugging
15
- * "empty stdout despite data[0].id is right there in the JSON" — the
16
- * hint pinpoints the segment that didn't resolve (e.g. "body is a string
17
- * — content-type was not application/json"). */
18
- export function extractByPathWithDiagnostic(
19
- obj: unknown,
20
- path: string,
21
- ): { value: unknown; resolved: string[]; failedAt?: string; reason?: string } {
22
- const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
23
- const resolved: string[] = [];
24
- let current: unknown = obj;
25
- for (const seg of segments) {
26
- if (current === null || current === undefined) {
27
- return { value: undefined, resolved, failedAt: seg, reason: "previous segment resolved to null/undefined" };
28
- }
29
- if (Array.isArray(current)) {
30
- // ARV-207: `length` resolves to the array length (jq-style). This
31
- // lets `--json-path data.items.length` answer "is the list non-empty?"
32
- // in one call — the harvest flow needs it ('length>0 → has data;
33
- // length==0 → seed') and previously had to shell out to jq.
34
- if (seg === "length") {
35
- current = current.length;
36
- resolved.push(seg);
37
- continue;
38
- }
39
- const idx = parseInt(seg, 10);
40
- if (isNaN(idx)) {
41
- return { value: undefined, resolved, failedAt: seg, reason: `expected an array index, got non-numeric segment "${seg}" (hint: use 'length' for array size)` };
42
- }
43
- if (idx < 0 || idx >= current.length) {
44
- return { value: undefined, resolved, failedAt: seg, reason: `array index ${idx} out of bounds (length ${current.length})` };
45
- }
46
- current = current[idx];
47
- } else if (typeof current === "object") {
48
- const obj = current as Record<string, unknown>;
49
- if (!(seg in obj)) {
50
- const keys = Object.keys(obj).slice(0, 8).join(", ");
51
- return { value: undefined, resolved, failedAt: seg, reason: `key "${seg}" not in object (keys: ${keys || "<empty>"})` };
52
- }
53
- current = obj[seg];
54
- } else {
55
- return {
56
- value: undefined,
57
- resolved,
58
- failedAt: seg,
59
- reason: `cannot traverse "${seg}" — body is a ${typeof current === "string" ? "string (content-type may not be application/json)" : typeof current}`,
60
- };
61
- }
62
- resolved.push(seg);
63
- }
64
- return { value: current, resolved };
65
- }
66
-
67
- export interface SendAdHocRequestOptions {
68
- method: string;
69
- url: string;
70
- headers?: Record<string, string>;
71
- body?: string;
72
- timeout?: number;
73
- envName?: string;
74
- collectionName?: string;
75
- jsonPath?: string;
76
- maxResponseChars?: number;
77
- dbPath?: string;
78
- searchDir?: string;
79
- /** Extra vars merged on top of env (e.g. captured values from a stored run). */
80
- extraVars?: Record<string, unknown>;
81
- /** When true, resolve interpolation but do not actually send the request. */
82
- dryRun?: boolean;
83
- /** ARV-149: when true, send the body as `application/x-www-form-urlencoded`.
84
- * Parses `body` as JSON to lift fields, then re-encodes with bracket notation
85
- * (Stripe-style nested keys). If `body` isn't JSON-parseable it's passed
86
- * through verbatim, and only the Content-Type header is set. */
87
- form?: boolean;
88
- }
89
-
90
- export interface SendAdHocRequestResult {
91
- status: number;
92
- headers: Record<string, string>;
93
- body: unknown;
94
- duration_ms: number;
95
- /** ARV-70: when --json-path failed to resolve, this carries which
96
- * segment broke and why so the CLI can surface a hint on stderr. */
97
- jsonPathDiagnostic?: { resolved: string[]; failedAt?: string; reason?: string };
98
- }
99
-
100
- export interface ResolvedRequest {
101
- method: string;
102
- url: string;
103
- headers: Record<string, string>;
104
- body?: string;
105
- }
106
-
107
- export async function resolveAdHocRequest(options: SendAdHocRequestOptions): Promise<ResolvedRequest> {
108
- let searchDir = options.searchDir ?? process.cwd();
109
- if (options.collectionName) {
110
- getDb(options.dbPath);
111
- const col = findCollectionByNameOrId(options.collectionName);
112
- if (!col) {
113
- throw new Error(`API '${options.collectionName}' is not registered. Run \`zond add api <name> --base-url <url>\` first, or check the name with \`zond db collections\`.`);
114
- }
115
- if (col.base_dir) searchDir = col.base_dir;
116
- }
117
- const envVars = await loadEnvironment(options.envName, searchDir);
118
- const vars = options.extraVars ? { ...envVars, ...options.extraVars } : envVars;
119
-
120
- // Auto-prefix base_url for relative paths when --api is in play.
121
- // Mirror the YAML-runner ergonomics: `zond request --api jp GET /users/1`
122
- // should work the same as `... GET '{{base_url}}/users/1'`. We touch the URL
123
- // only when it's clearly relative (leading "/") and has no scheme/template
124
- // already, so absolute URLs and explicit {{var}} templates pass through.
125
- let urlToResolve = options.url;
126
- if (
127
- options.collectionName
128
- && typeof vars.base_url === "string"
129
- && vars.base_url.length > 0
130
- && urlToResolve.startsWith("/")
131
- && !urlToResolve.startsWith("//")
132
- ) {
133
- const base = vars.base_url.replace(/\/+$/, "");
134
- urlToResolve = `${base}${urlToResolve}`;
135
- }
136
-
137
- const resolvedUrl = substituteString(urlToResolve, vars) as string;
138
- const parsedHeaders = options.headers ?? {};
139
- const resolvedHeaders = Object.keys(parsedHeaders).length > 0 ? substituteDeep(parsedHeaders, vars) : {};
140
- let resolvedBody = options.body ? substituteString(options.body, vars) as string : undefined;
141
-
142
- const finalHeaders: Record<string, string> = { ...resolvedHeaders };
143
- const hasContentType =
144
- finalHeaders["Content-Type"] !== undefined || finalHeaders["content-type"] !== undefined;
145
-
146
- // ARV-149: `--form` (or auto-detection from spec content type) re-encodes
147
- // the JSON body as `application/x-www-form-urlencoded` with bracket
148
- // notation. Stripe v1 and other Rails/PHP-style APIs declare ONLY form
149
- // bodies on their mutating endpoints — sending JSON yields a 400
150
- // "check that your POST content type is application/x-www-form-urlencoded".
151
- if (resolvedBody && options.form) {
152
- try {
153
- const parsed = JSON.parse(resolvedBody);
154
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
155
- resolvedBody = encodeFormBody(parsed as Record<string, unknown>);
156
- }
157
- } catch {
158
- // Body isn't JSON — assume it's already urlencoded; pass through verbatim.
159
- }
160
- if (!hasContentType) {
161
- finalHeaders["Content-Type"] = "application/x-www-form-urlencoded";
162
- }
163
- } else if (resolvedBody && !hasContentType) {
164
- try {
165
- JSON.parse(resolvedBody);
166
- finalHeaders["Content-Type"] = "application/json";
167
- } catch {
168
- // Not JSON — don't set content-type, let server decide
169
- }
170
- }
171
-
172
- // TASK-231: when --api resolved an env with `auth_token`, auto-inject the
173
- // standard `Authorization: Bearer …` header. This mirrors the YAML runner's
174
- // behaviour (probes/suites template the same header from the security
175
- // scheme) so `zond request --api X GET /…` doesn't 401 just because the
176
- // user didn't repeat `--header "Authorization: Bearer {{auth_token}}"`.
177
- // User-supplied Authorization always wins.
178
- if (
179
- options.collectionName
180
- && typeof vars.auth_token === "string"
181
- && vars.auth_token.length > 0
182
- && !hasHeaderCI(finalHeaders, "Authorization")
183
- ) {
184
- finalHeaders["Authorization"] = `Bearer ${vars.auth_token}`;
185
- }
186
-
187
- return {
188
- method: options.method,
189
- url: resolvedUrl,
190
- headers: finalHeaders,
191
- ...(resolvedBody !== undefined ? { body: resolvedBody } : {}),
192
- };
193
- }
194
-
195
- // Note: `options.form` is consumed inside `resolveAdHocRequest` itself —
196
- // the encoded `body` string and Content-Type are baked into `finalHeaders`.
197
- // `sendAdHocRequest` doesn't need to forward the flag separately.
198
-
199
- export async function sendAdHocRequest(options: SendAdHocRequestOptions): Promise<SendAdHocRequestResult> {
200
- const resolved = await resolveAdHocRequest(options);
201
-
202
- const response = await executeRequest(
203
- {
204
- method: resolved.method,
205
- url: resolved.url,
206
- headers: resolved.headers,
207
- body: resolved.body,
208
- },
209
- options.timeout ? { timeout: options.timeout } : undefined,
210
- );
211
-
212
- let responseBody: unknown = response.body_parsed ?? response.body;
213
- let jsonPathDiagnostic: SendAdHocRequestResult["jsonPathDiagnostic"];
214
-
215
- if (options.jsonPath && responseBody !== undefined) {
216
- const diag = extractByPathWithDiagnostic(responseBody, options.jsonPath);
217
- responseBody = diag.value;
218
- if (diag.value === undefined && diag.failedAt) {
219
- jsonPathDiagnostic = { resolved: diag.resolved, failedAt: diag.failedAt, reason: diag.reason };
220
- }
221
- }
222
-
223
- const result: SendAdHocRequestResult = {
224
- status: response.status,
225
- headers: response.headers,
226
- body: responseBody,
227
- duration_ms: response.duration_ms,
228
- ...(jsonPathDiagnostic ? { jsonPathDiagnostic } : {}),
229
- };
230
-
231
- return result;
232
- }
@@ -1,65 +0,0 @@
1
- const DIRECTIVES = new Set(["concat", "append", "length", "get", "first", "map_field"]);
2
-
3
- export function applyTransform(directive: unknown): unknown {
4
- if (typeof directive !== "object" || directive === null || Array.isArray(directive)) {
5
- return directive;
6
- }
7
-
8
- const obj = directive as Record<string, unknown>;
9
- const keys = Object.keys(obj);
10
-
11
- if (keys.length !== 1 || !DIRECTIVES.has(keys[0]!)) {
12
- return directive;
13
- }
14
-
15
- const op = keys[0]!;
16
- const arg = obj[op];
17
-
18
- switch (op) {
19
- case "concat": {
20
- if (!Array.isArray(arg)) return directive;
21
- const result: unknown[] = [];
22
- for (const item of arg) {
23
- if (Array.isArray(item)) result.push(...item);
24
- else result.push(item);
25
- }
26
- return result;
27
- }
28
- case "append": {
29
- if (!Array.isArray(arg) || arg.length < 2) return directive;
30
- const arr = Array.isArray(arg[0]) ? [...arg[0]] : [];
31
- return [...arr, ...arg.slice(1)];
32
- }
33
- case "length": {
34
- if (Array.isArray(arg)) return arg.length;
35
- if (typeof arg === "string") return arg.length;
36
- return 0;
37
- }
38
- case "get": {
39
- if (!Array.isArray(arg) || arg.length < 2) return directive;
40
- const [source, index] = arg;
41
- if (Array.isArray(source) && typeof index === "number") return source[index];
42
- if (typeof source === "object" && source !== null && typeof index === "string") {
43
- return (source as Record<string, unknown>)[index];
44
- }
45
- return undefined;
46
- }
47
- case "first": {
48
- if (Array.isArray(arg)) return arg[0];
49
- return undefined;
50
- }
51
- case "map_field": {
52
- if (!Array.isArray(arg) || arg.length < 2) return directive;
53
- const [items, field] = arg;
54
- if (!Array.isArray(items) || typeof field !== "string") return directive;
55
- return items.map((item) => {
56
- if (typeof item === "object" && item !== null) {
57
- return (item as Record<string, unknown>)[field];
58
- }
59
- return undefined;
60
- });
61
- }
62
- default:
63
- return directive;
64
- }
65
- }