@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
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Unified operation filter (m-15 ARV-9).
3
+ *
4
+ * Parses `--include`/`--exclude` filter specs in a single grammar so
5
+ * `zond run`, `zond checks`, `zond probe`, and `zond generate` all
6
+ * accept the same `<selector>:<value>` strings without each command
7
+ * inventing its own flag set.
8
+ *
9
+ * Grammar:
10
+ *
11
+ * <spec> := <selector> ":" <value>
12
+ * <selector> := "path" | "method" | "tag" | "operation-id" | "operationId"
13
+ * <value>:
14
+ * path — POSIX-style regex matched against `op.path`
15
+ * method — comma-separated HTTP methods, case-insensitive
16
+ * tag — comma-separated tag names, exact match (case-sensitive)
17
+ * operation-id — POSIX-style regex matched against `op.operationId`
18
+ *
19
+ * Semantics:
20
+ * - Multiple `--include` flags combine with OR (op passes if it
21
+ * matches *any* include); when no include is given, every op is
22
+ * considered included.
23
+ * - `--exclude` always removes a match — combines with OR too.
24
+ * - Excludes are evaluated *after* includes.
25
+ *
26
+ * Errors are returned in a `errors[]` array on the compile result so
27
+ * the CLI can surface a friendly multi-line message instead of a
28
+ * stack trace (AC #4).
29
+ */
30
+ import type { EndpointInfo } from "../generator/types.ts";
31
+
32
+ export type SelectorKind = "path" | "method" | "tag" | "operation-id";
33
+
34
+ const SELECTOR_ALIASES: Record<string, SelectorKind> = {
35
+ path: "path",
36
+ method: "method",
37
+ tag: "tag",
38
+ "operation-id": "operation-id",
39
+ operationid: "operation-id",
40
+ operation_id: "operation-id",
41
+ };
42
+
43
+ export interface ParsedSelector {
44
+ kind: SelectorKind;
45
+ raw: string;
46
+ /** Regex form for `path` and `operation-id` selectors. */
47
+ pattern?: RegExp;
48
+ /** Lowercase token list for `method`/`tag` selectors. */
49
+ values?: string[];
50
+ }
51
+
52
+ export type ParseResult =
53
+ | { ok: true; selector: ParsedSelector }
54
+ | { ok: false; error: string };
55
+
56
+ export function parseFilterSpec(spec: string): ParseResult {
57
+ const idx = spec.indexOf(":");
58
+ if (idx <= 0) {
59
+ return { ok: false, error: `Filter "${spec}": expected "<selector>:<value>" (e.g. path:/users/.*)` };
60
+ }
61
+ const head = spec.slice(0, idx).trim().toLowerCase();
62
+ const tail = spec.slice(idx + 1).trim();
63
+ if (tail.length === 0) {
64
+ return { ok: false, error: `Filter "${spec}": value is empty after "${head}:"` };
65
+ }
66
+ const kind = SELECTOR_ALIASES[head];
67
+ if (!kind) {
68
+ const known = Object.keys(SELECTOR_ALIASES).filter((k) => k === SELECTOR_ALIASES[k]).join(", ");
69
+ return { ok: false, error: `Filter "${spec}": unknown selector "${head}". Known: ${known}` };
70
+ }
71
+
72
+ if (kind === "path" || kind === "operation-id") {
73
+ let pattern: RegExp;
74
+ try {
75
+ pattern = new RegExp(tail);
76
+ } catch (err) {
77
+ return { ok: false, error: `Filter "${spec}": invalid regex — ${(err as Error).message}` };
78
+ }
79
+ return { ok: true, selector: { kind, raw: spec, pattern } };
80
+ }
81
+
82
+ // method / tag — comma-separated.
83
+ const values = tail.split(",").map((v) => v.trim()).filter(Boolean);
84
+ if (values.length === 0) {
85
+ return { ok: false, error: `Filter "${spec}": no values after "${head}:"` };
86
+ }
87
+ if (kind === "method") {
88
+ return { ok: true, selector: { kind, raw: spec, values: values.map((v) => v.toUpperCase()) } };
89
+ }
90
+ return { ok: true, selector: { kind, raw: spec, values } };
91
+ }
92
+
93
+ function selectorMatches(sel: ParsedSelector, op: EndpointInfo): boolean {
94
+ switch (sel.kind) {
95
+ case "path":
96
+ return sel.pattern!.test(op.path);
97
+ case "method":
98
+ return sel.values!.includes(op.method.toUpperCase());
99
+ case "tag":
100
+ return op.tags.some((t) => sel.values!.includes(t));
101
+ case "operation-id":
102
+ return op.operationId !== undefined && sel.pattern!.test(op.operationId);
103
+ }
104
+ }
105
+
106
+ export interface CompileFilterOptions {
107
+ includes?: string[];
108
+ excludes?: string[];
109
+ }
110
+
111
+ export interface CompiledFilter {
112
+ filter: (op: EndpointInfo) => boolean;
113
+ errors: string[];
114
+ /** Parsed selectors — handy for debug `--explain` output. */
115
+ parsed: { includes: ParsedSelector[]; excludes: ParsedSelector[] };
116
+ }
117
+
118
+ export function compileOperationFilter(opts: CompileFilterOptions = {}): CompiledFilter {
119
+ const errors: string[] = [];
120
+ const includes: ParsedSelector[] = [];
121
+ const excludes: ParsedSelector[] = [];
122
+ for (const raw of opts.includes ?? []) {
123
+ const r = parseFilterSpec(raw);
124
+ if (r.ok) includes.push(r.selector);
125
+ else errors.push(r.error);
126
+ }
127
+ for (const raw of opts.excludes ?? []) {
128
+ const r = parseFilterSpec(raw);
129
+ if (r.ok) excludes.push(r.selector);
130
+ else errors.push(r.error);
131
+ }
132
+ const filter = (op: EndpointInfo): boolean => {
133
+ if (includes.length > 0) {
134
+ const passInclude = includes.some((s) => selectorMatches(s, op));
135
+ if (!passInclude) return false;
136
+ }
137
+ for (const s of excludes) {
138
+ if (selectorMatches(s, op)) return false;
139
+ }
140
+ return true;
141
+ };
142
+ return { filter, errors, parsed: { includes, excludes } };
143
+ }
144
+
@@ -1,8 +1,171 @@
1
- import { resolve, join } from "path";
2
- import { mkdirSync, writeFileSync, existsSync, readFileSync } from "fs";
1
+ import { resolve, join, relative } from "path";
2
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "fs";
3
3
  import { getDb } from "../db/schema.ts";
4
4
  import { createCollection, deleteCollection, findCollectionByNameOrId, normalizePath } from "../db/queries.ts";
5
- import { readOpenApiSpec, extractEndpoints } from "./generator/index.ts";
5
+ import {
6
+ readOpenApiSpec,
7
+ extractEndpoints,
8
+ extractSecuritySchemes,
9
+ buildCatalog,
10
+ serializeCatalog,
11
+ buildApiResourceMap,
12
+ serializeApiResourceMap,
13
+ buildApiFixtureManifest,
14
+ serializeApiFixtureManifest,
15
+ } from "./generator/index.ts";
16
+ import { decycleSchema } from "./generator/schema-utils.ts";
17
+ import { schemeVarName } from "./generator/suite-generator.ts";
18
+ import type { SecuritySchemeInfo } from "./generator/types.ts";
19
+ import { hashSpec } from "./meta/meta-store.ts";
20
+ import { findWorkspaceRoot } from "./workspace/root.ts";
21
+ import { recordGeneratedFiles, type RecordInput } from "./workspace/manifest.ts";
22
+ import { CANONICAL_IDENTITY_KEYS } from "./identity/identity-file.ts";
23
+
24
+ /** Filename of the dereferenced spec snapshot inside `apis/<name>/`. */
25
+ export const SPEC_SNAPSHOT_FILENAME = "spec.json";
26
+
27
+ interface WriteArtifactsParams {
28
+ /** Dereferenced OpenAPI document. */
29
+ doc: unknown;
30
+ /** Absolute path to apis/<name>/. */
31
+ baseDir: string;
32
+ /** Collection name (goes into the catalog header). */
33
+ apiName: string;
34
+ /** Resolved server URL or "". */
35
+ baseUrl: string;
36
+ /** Absolute workspace root, used to compute the relative specSource. */
37
+ workspaceRoot: string;
38
+ /** Caller label for manifest entries (defaults to "zond add api"). */
39
+ by?: string;
40
+ }
41
+
42
+ /**
43
+ * Snapshot the dereferenced spec into `apis/<name>/spec.json` and emit the
44
+ * three derived artifacts (`.api-catalog.yaml`, `.api-resources.yaml`,
45
+ * `.api-fixtures.yaml`). Pure side-effect; safe to call from `setupApi` at
46
+ * register time and from `refreshApi` for re-snapshot.
47
+ */
48
+ export function writeArtifactsFromDoc(params: WriteArtifactsParams): void {
49
+ const { doc, baseDir, apiName, baseUrl, workspaceRoot, by = "zond add api" } = params;
50
+ const localSpecAbsPath = join(baseDir, SPEC_SNAPSHOT_FILENAME);
51
+ // Pass through decycleSchema first — large specs (Stripe, GitHub) contain
52
+ // mutually-recursive `$ref` chains that resolve to true object cycles after
53
+ // dereference, and raw JSON.stringify crashes on those with "cannot
54
+ // serialize cyclic structures" (ARV-145). decycleSchema collapses the
55
+ // second visit to `{ "x-circular": true }` (vendor-extension sentinel —
56
+ // NOT `$ref`, otherwise the parser tries to resolve "[Circular]" as a
57
+ // file path when re-reading spec.json, ARV-146) so the on-disk snapshot
58
+ // is self-contained, parser-safe JSON.
59
+ let serialized: string;
60
+ try {
61
+ serialized = JSON.stringify(decycleSchema(doc), null, 2);
62
+ } catch (err) {
63
+ const m = (err as Error).message;
64
+ throw new Error(
65
+ `spec_serialize_failed: could not serialize dereferenced spec for '${apiName}' — ${m}. ` +
66
+ `This usually means the spec contains a structure decycleSchema could not collapse; please open an issue with the spec URL.`,
67
+ );
68
+ }
69
+ writeFileSync(localSpecAbsPath, serialized + "\n", "utf-8");
70
+
71
+ const endpoints = extractEndpoints(doc as any);
72
+ const securitySchemes = extractSecuritySchemes(doc as any);
73
+ // Hash the on-disk file bytes — this is what `zond doctor` re-hashes when
74
+ // checking artifact freshness (TASK-215). Both sides now read the decycled
75
+ // form: setup-api writes it here, doctor re-reads the same file.
76
+ const specHash = hashSpec(readFileSync(localSpecAbsPath, "utf-8"));
77
+ const localSpecRelPath = relative(workspaceRoot, localSpecAbsPath).replace(/\\/g, "/");
78
+
79
+ const catalog = buildCatalog({
80
+ endpoints,
81
+ securitySchemes,
82
+ specSource: localSpecRelPath,
83
+ specHash,
84
+ apiName,
85
+ apiVersion: (doc as any).info?.version,
86
+ baseUrl,
87
+ });
88
+ const catalogPath = join(baseDir, ".api-catalog.yaml");
89
+ writeFileSync(catalogPath, serializeCatalog(catalog), "utf-8");
90
+
91
+ const resources = buildApiResourceMap({ endpoints, specHash });
92
+ const resourcesPath = join(baseDir, ".api-resources.yaml");
93
+ writeFileSync(resourcesPath, serializeApiResourceMap(resources), "utf-8");
94
+
95
+ const fixtures = buildApiFixtureManifest({
96
+ endpoints,
97
+ securitySchemes,
98
+ baseUrl: baseUrl || undefined,
99
+ specHash,
100
+ resourceMap: resources,
101
+ });
102
+ const fixturesPath = join(baseDir, ".api-fixtures.yaml");
103
+ writeFileSync(fixturesPath, serializeApiFixtureManifest(fixtures), "utf-8");
104
+
105
+ // Record artifacts in .zond/manifest.json (TASK-156).
106
+ try {
107
+ const entries: RecordInput[] = [
108
+ { path: localSpecAbsPath, by, api: apiName, category: "spec" },
109
+ { path: catalogPath, by, api: apiName, category: "catalog" },
110
+ { path: resourcesPath, by, api: apiName, category: "resources" },
111
+ { path: fixturesPath, by, api: apiName, category: "fixtures" },
112
+ ];
113
+ recordGeneratedFiles(workspaceRoot, entries);
114
+ } catch {
115
+ // best-effort
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Resolve a `collections.openapi_spec` value to a concrete file path the
121
+ * caller can read. Throws on a legacy / broken workspace so the user gets
122
+ * a single clear instruction instead of a downstream ENOENT.
123
+ *
124
+ * Resolution order:
125
+ * 1. URL (http/https) — return as-is.
126
+ * 2. Workspace-relative path (e.g. `apis/<name>/spec.json`) that exists.
127
+ * 3. Absolute filesystem path that exists. Treated as legacy: the spec
128
+ * is outside the workspace and not snapshotted into `apis/<name>/`.
129
+ * We let it through, but `assertLocalSpec` (used by run/report/doctor)
130
+ * will reject it.
131
+ * 4. Otherwise — throw a "legacy / stale workspace" error pointing at
132
+ * `zond refresh-api`.
133
+ */
134
+ export function resolveCollectionSpec(specRef: string): string {
135
+ if (/^https?:\/\//i.test(specRef)) return specRef;
136
+ const root = findWorkspaceRoot().root;
137
+ const local = resolve(root, specRef);
138
+ if (existsSync(local)) return local;
139
+ if (specRef.startsWith("/") && existsSync(specRef)) return specRef;
140
+ throw new Error(
141
+ `Spec for this API is missing at ${local}` +
142
+ (specRef.startsWith("/") ? ` (DB recorded an external path: ${specRef})` : "") +
143
+ `. The workspace looks legacy or stale — run \`zond refresh-api <name> [--spec <path|url>]\` to re-snapshot.`,
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Strict variant for code paths that must read the workspace-local
149
+ * snapshot (run/report/doctor). Returns the local absolute path or
150
+ * throws — never returns an external URL or path.
151
+ */
152
+ export function assertLocalSpec(specRef: string, apiName: string): string {
153
+ if (/^https?:\/\//i.test(specRef)) {
154
+ throw new Error(
155
+ `API '${apiName}' has a remote spec recorded (${specRef}) but no local snapshot. ` +
156
+ `Run \`zond refresh-api ${apiName}\` to materialise apis/${apiName}/${SPEC_SNAPSHOT_FILENAME}.`,
157
+ );
158
+ }
159
+ const root = findWorkspaceRoot().root;
160
+ const local = resolve(root, specRef);
161
+ if (!existsSync(local)) {
162
+ throw new Error(
163
+ `Local spec missing for API '${apiName}' (expected ${local}). ` +
164
+ `Run \`zond refresh-api ${apiName}\` to regenerate it.`,
165
+ );
166
+ }
167
+ return local;
168
+ }
6
169
 
7
170
  function toYaml(vars: Record<string, string>): string {
8
171
  const lines: string[] = [];
@@ -31,9 +194,32 @@ export interface SetupApiResult {
31
194
  baseUrl: string;
32
195
  specEndpoints: number;
33
196
  pathParams?: Record<string, string>;
197
+ /** Auth-related env-var names auto-seeded as `@secret:<name>` (TASK-209). */
198
+ authVars?: string[];
34
199
  warnings?: string[];
35
200
  }
36
201
 
202
+ /**
203
+ * Walk the security schemes and derive the env-var names that
204
+ * `@readme/openapi-parser`-derived suites/probes will reference for auth
205
+ * tokens. Mirrors `getAuthHeaders` in src/core/probe/shared.ts:
206
+ * - HTTP bearer/basic/empty-scheme → schemeVarName(...) (default "auth_token")
207
+ * - apiKey in header named "Authorization" → schemeVarName(...)
208
+ * - apiKey in header (other name) → "api_key"
209
+ */
210
+ function deriveAuthVarNames(schemes: SecuritySchemeInfo[]): string[] {
211
+ const vars = new Set<string>();
212
+ for (const s of schemes) {
213
+ if (s.type === "http" && (s.scheme === "bearer" || s.scheme === "basic" || !s.scheme)) {
214
+ vars.add(schemeVarName(s, schemes));
215
+ } else if (s.type === "apiKey" && s.in === "header" && s.apiKeyName) {
216
+ if (s.apiKeyName === "Authorization") vars.add(schemeVarName(s, schemes));
217
+ else vars.add("api_key");
218
+ }
219
+ }
220
+ return [...vars];
221
+ }
222
+
37
223
  export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult> {
38
224
  const { spec, dbPath } = options;
39
225
 
@@ -46,11 +232,47 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
46
232
  const pathParams = new Map<string, string>();
47
233
  const warnings: string[] = [];
48
234
  let specTitle: string | undefined;
235
+ // The dereferenced doc is captured here so we can copy it into the
236
+ // workspace after the target dir is computed (below). We snapshot the
237
+ // *dereferenced* form so all consumers (probe-*, generate, describe) read
238
+ // a self-contained file — no external $ref resolution at runtime.
239
+ let dereferencedDoc: unknown = null;
240
+ let authVarNames: string[] = [];
49
241
  if (spec) {
50
242
  const doc = await readOpenApiSpec(spec, { insecure: options.insecure });
243
+ // Validate the document looks like OpenAPI/Swagger before we snapshot it.
244
+ // dereference() happily round-trips arbitrary JSON (e.g. a marketing-site
245
+ // landing payload), so without this guard `zond add api foo --spec
246
+ // https://example.com` silently registers a 0-endpoint API.
247
+ const docAny = doc as any;
248
+ const hasOpenApiField = typeof docAny?.openapi === "string";
249
+ const hasSwaggerField = typeof docAny?.swagger === "string";
250
+ if (!hasOpenApiField && !hasSwaggerField) {
251
+ throw new Error(
252
+ `Spec at ${spec} is not an OpenAPI/Swagger document — missing top-level 'openapi' (3.x) or 'swagger' (2.x) field. Check the URL points to the JSON spec, not the API root.`,
253
+ );
254
+ }
255
+ dereferencedDoc = doc;
51
256
  openapiSpec = spec;
52
257
  if ((doc as any).servers?.[0]?.url) {
53
258
  baseUrl = (doc as any).servers[0].url;
259
+ // Resolve OpenAPI server variables (e.g. {region}) using their declared defaults.
260
+ // Without this, the raw placeholder ends up in .env.yaml and causes cryptic TLS
261
+ // errors because the hostname literally contains "{region}".
262
+ const serverVars = (doc as any).servers[0].variables as
263
+ Record<string, { default?: string }> | undefined;
264
+ if (serverVars && baseUrl.includes("{")) {
265
+ baseUrl = baseUrl.replace(/\{([^}]+)\}/g, (_: string, name: string) =>
266
+ serverVars[name]?.default ?? `{${name}}`
267
+ );
268
+ }
269
+ // Warn if any placeholder remains unresolved (spec didn't provide a default).
270
+ const unresolved = [...baseUrl.matchAll(/\{([^}]+)\}/g)].map(m => m[1]);
271
+ if (unresolved.length > 0) {
272
+ warnings.push(
273
+ `base_url contains unresolved server variable${unresolved.length === 1 ? "" : "s"}: ${unresolved.map(v => `{${v}}`).join(", ")}. Edit .env.yaml and replace with a concrete value.`,
274
+ );
275
+ }
54
276
  }
55
277
  if (baseUrl && !baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
56
278
  warnings.push(`Spec server URL "${baseUrl}" is relative — requests will fail without a host. Override with envVars: {"base_url": "https://your-host${baseUrl}"}`);
@@ -58,16 +280,28 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
58
280
  specTitle = (doc as any).info?.title;
59
281
  const endpoints = extractEndpoints(doc);
60
282
  endpointCount = endpoints.length;
283
+ authVarNames = deriveAuthVarNames(extractSecuritySchemes(doc));
61
284
 
62
- // Collect unique path parameters with default values
285
+ if (endpointCount === 0) {
286
+ const hasPaths = docAny?.paths && typeof docAny.paths === "object" && Object.keys(docAny.paths).length > 0;
287
+ warnings.push(
288
+ hasPaths
289
+ ? `Spec declares paths but no operations were extracted — every method may be filtered out (deprecated, unsupported method, etc.). Verify with \`zond catalog --api <name>\`.`
290
+ : `Spec contains 0 endpoints — 'paths' field is empty or missing. generate/probe/checks will produce nothing until the spec is fixed or replaced.`,
291
+ );
292
+ }
293
+
294
+ // Collect unique path parameters. The default is empty string so that
295
+ // generated `skip_if: "{{<id>}} =="` checks auto-skip until the user
296
+ // fills the value in .env.yaml (TASK-210). Spec-provided examples are
297
+ // kept verbatim so they are still useful as concrete fixtures.
63
298
  for (const ep of endpoints) {
64
299
  for (const param of (ep.parameters ?? []).filter(p => p.in === "path")) {
65
300
  if (pathParams.has(param.name)) continue;
66
301
  const schema = param.schema as any;
67
302
  if (param.example !== undefined) pathParams.set(param.name, String(param.example));
68
303
  else if (schema?.example !== undefined) pathParams.set(param.name, String(schema.example));
69
- else if (schema?.type === "integer" || schema?.type === "number") pathParams.set(param.name, "1");
70
- else pathParams.set(param.name, "example");
304
+ else pathParams.set(param.name, "");
71
305
  }
72
306
  }
73
307
  }
@@ -90,12 +324,68 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
90
324
 
91
325
  // Sanitize name for directory use
92
326
  const dirName = name.replace(/[^a-zA-Z0-9_\-\.]/g, "-").toLowerCase();
93
- const baseDir = resolve(options.dir ?? `./apis/${dirName}/`);
327
+ const baseDir = options.dir
328
+ ? resolve(options.dir)
329
+ : resolve(findWorkspaceRoot().root, `apis/${dirName}/`);
94
330
  const testPath = join(baseDir, "tests");
95
331
 
332
+ // Track whether we created baseDir from scratch so we can clean it up on
333
+ // failure — without this, a crash mid-setup (e.g. JSON.stringify on a
334
+ // cyclic spec, ARV-145) leaves apis/<slug>/tests/ behind and confuses the
335
+ // next `zond add api` invocation.
336
+ const baseDirPreExisted = existsSync(baseDir);
337
+
96
338
  // Create directories
97
339
  mkdirSync(testPath, { recursive: true });
98
340
 
341
+ try {
342
+ return await finalizeSetup({
343
+ name,
344
+ baseDir,
345
+ testPath,
346
+ baseUrl,
347
+ pathParams,
348
+ authVarNames,
349
+ envVarsOverride: options.envVars,
350
+ spec,
351
+ dereferencedDoc,
352
+ openapiSpec,
353
+ endpointCount,
354
+ warnings,
355
+ });
356
+ } catch (err) {
357
+ // Roll back partial filesystem state (apis/<slug>/tests/, spec.json, etc.)
358
+ // when we created the dir from scratch. Without this, the next
359
+ // `zond add api <same-name>` would still find a stale dir and demand
360
+ // --force, even though no collection was actually registered. ARV-145.
361
+ if (!baseDirPreExisted) {
362
+ try { rmSync(baseDir, { recursive: true, force: true }); } catch { /* best-effort */ }
363
+ }
364
+ throw err;
365
+ }
366
+ }
367
+
368
+ interface FinalizeSetupParams {
369
+ name: string;
370
+ baseDir: string;
371
+ testPath: string;
372
+ baseUrl: string;
373
+ pathParams: Map<string, string>;
374
+ authVarNames: string[];
375
+ envVarsOverride?: Record<string, string>;
376
+ spec?: string;
377
+ dereferencedDoc: unknown;
378
+ openapiSpec: string | null;
379
+ endpointCount: number;
380
+ warnings: string[];
381
+ }
382
+
383
+ async function finalizeSetup(p: FinalizeSetupParams): Promise<SetupApiResult> {
384
+ const {
385
+ name, baseDir, testPath, baseUrl, pathParams, authVarNames,
386
+ envVarsOverride, spec, dereferencedDoc, openapiSpec, endpointCount, warnings,
387
+ } = p;
388
+
99
389
  // Build environment variables
100
390
  const envVars: Record<string, string> = {};
101
391
  if (baseUrl) envVars.base_url = baseUrl;
@@ -103,8 +393,33 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
103
393
  for (const [k, v] of pathParams) {
104
394
  if (!(k in envVars)) envVars[k] = v;
105
395
  }
106
- if (options.envVars) {
107
- Object.assign(envVars, options.envVars);
396
+ // Auto-wire auth env-vars to .secrets.yaml so generated suites and probes
397
+ // resolve `{{auth_token}}` (etc.) without manual editing of .env.yaml
398
+ // (TASK-209). The matching `<var>: ""` placeholder is seeded into
399
+ // .secrets.yaml below — the user only fills the secret value.
400
+ for (const v of authVarNames) {
401
+ if (!(v in envVars)) envVars[v] = `@secret:${v}`;
402
+ }
403
+ // ARV-201 (R10/F2): when the spec declares no `components.securitySchemes`
404
+ // (GitHub publishes its OpenAPI this way), `deriveAuthVarNames` returns []
405
+ // and the loop above is a no-op — yet `zond request --api <name>` knows
406
+ // to attach `Authorization: Bearer <auth_token>` if the env carries an
407
+ // `auth_token`. Mirror the `.secrets.yaml` fallback (which already seeds
408
+ // `auth_token: ""` when authVarNames is empty) into `.env.yaml` so users
409
+ // do not need to hand-add `auth_token: "@secret:auth_token"` just to
410
+ // surface the Bearer header on bare specs.
411
+ if (authVarNames.length === 0 && !("auth_token" in envVars)) {
412
+ envVars.auth_token = "@secret:auth_token";
413
+ }
414
+ if (envVarsOverride) {
415
+ Object.assign(envVars, envVarsOverride);
416
+ }
417
+
418
+ // Spec-less registration is allowed, but we need a base_url from somewhere
419
+ // (server URL extracted from the spec, or envVars.base_url passed in by the
420
+ // caller). Without it the API is useless — `zond run` can't resolve {{base_url}}.
421
+ if (!spec && !envVars.base_url) {
422
+ throw new Error("setupApi requires --spec or envVars.base_url to register an API");
108
423
  }
109
424
 
110
425
  // Write .env.yaml in base_dir
@@ -113,26 +428,112 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
113
428
  writeFileSync(envFilePath, toYaml(envVars) + "\n", "utf-8");
114
429
  }
115
430
 
116
- // Create/update .gitignore to exclude env files
431
+ // Create/update .gitignore to exclude env / secret files
117
432
  const gitignorePath = join(baseDir, ".gitignore");
118
- const gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
433
+ let gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
434
+ let gitignoreDirty = false;
119
435
  if (!gitignoreContent.includes(".env*.yaml")) {
120
- writeFileSync(
121
- gitignorePath,
122
- gitignoreContent + (gitignoreContent.endsWith("\n") || !gitignoreContent ? "" : "\n") + ".env*.yaml\n",
123
- "utf-8",
124
- );
436
+ gitignoreContent +=
437
+ (gitignoreContent.endsWith("\n") || !gitignoreContent ? "" : "\n") + ".env*.yaml\n";
438
+ gitignoreDirty = true;
439
+ }
440
+ // TASK-170 (m-10): keep `.secrets.yaml` git-invisible. Older `.env*.yaml`
441
+ // pattern matched it accidentally; pin it explicitly so a future glob
442
+ // narrowing can't regress.
443
+ if (!gitignoreContent.includes(".secrets.yaml")) {
444
+ gitignoreContent += ".secrets.yaml\n";
445
+ gitignoreDirty = true;
446
+ }
447
+ // TASK-174 (m-10): identity values are not secrets but they identify
448
+ // the user's account; keep them out of git too.
449
+ if (!gitignoreContent.includes(".identity.yaml")) {
450
+ gitignoreContent += ".identity.yaml\n";
451
+ gitignoreDirty = true;
452
+ }
453
+ if (gitignoreDirty) {
454
+ writeFileSync(gitignorePath, gitignoreContent, "utf-8");
455
+ }
456
+
457
+ // Seed `.secrets.yaml` placeholder once. The file lives gitignored
458
+ // alongside `.env.yaml`; values placed here are auto-registered with
459
+ // the SecretRegistry on load and never appear in artifacts.
460
+ const secretsPath = join(baseDir, ".secrets.yaml");
461
+ if (!existsSync(secretsPath)) {
462
+ const seedKeys = authVarNames.length > 0 ? authVarNames : ["auth_token"];
463
+ const lines = [
464
+ "# .secrets.yaml — gitignored, holds raw secret values.",
465
+ "# Reference these in .env.yaml as @secret:<key>.",
466
+ "# Values here are auto-registered for redaction in DB writes,",
467
+ "# HTML/JSON/JUnit reports, case-studies, and probe digests.",
468
+ ];
469
+ for (const k of seedKeys) lines.push(`${k}: "" # required for live probes`);
470
+ lines.push("");
471
+ writeFileSync(secretsPath, lines.join("\n"), "utf-8");
472
+ }
473
+
474
+ // TASK-174 (m-10): seed `.identity.yaml` with placeholders for any
475
+ // canonical identity-keys that appear as path-params in the spec. The
476
+ // file is gitignored — values are visible locally for triage and
477
+ // hidden from outbound shares only when --redact-identity is set.
478
+ const identityKeys = [...pathParams.keys()].filter((k) =>
479
+ CANONICAL_IDENTITY_KEYS.has(k),
480
+ );
481
+ if (identityKeys.length > 0) {
482
+ const identityPath = join(baseDir, ".identity.yaml");
483
+ if (!existsSync(identityPath)) {
484
+ const lines = [
485
+ "# .identity.yaml — gitignored, holds non-secret-but-identifying values.",
486
+ "# Reference these in .env.yaml as @identity:<key>.",
487
+ "# Values are visible locally and in case-study drafts; pass",
488
+ "# --redact-identity (TASK-173) to swap them for placeholders when",
489
+ "# sharing reports outbound.",
490
+ ];
491
+ for (const k of identityKeys.sort()) {
492
+ lines.push(`${k}: "" # fill with your ${k}`);
493
+ }
494
+ lines.push("");
495
+ writeFileSync(identityPath, lines.join("\n"), "utf-8");
496
+ }
497
+ }
498
+
499
+ const workspaceRoot = findWorkspaceRoot().root;
500
+
501
+ // Snapshot the dereferenced spec into apis/<name>/spec.json so all later
502
+ // commands (catalog, describe, generate, probe-*) read a self-contained
503
+ // local file. The spec lives inside the workspace and is git-trackable;
504
+ // an external --spec path is only consulted at register/refresh time.
505
+ let localSpecAbsPath: string | null = null;
506
+ if (dereferencedDoc) {
507
+ localSpecAbsPath = join(baseDir, SPEC_SNAPSHOT_FILENAME);
508
+ writeArtifactsFromDoc({
509
+ doc: dereferencedDoc,
510
+ baseDir,
511
+ apiName: name,
512
+ baseUrl,
513
+ workspaceRoot,
514
+ });
125
515
  }
126
516
 
127
517
  const normalizedTestPath = normalizePath(testPath);
128
518
  const normalizedBaseDir = normalizePath(baseDir);
129
519
 
520
+ // Persist the workspace-relative path to the local snapshot in
521
+ // collections.openapi_spec so we don't rely on the user's external path
522
+ // sticking around. Falls back to the external path only when the snapshot
523
+ // could not be created (no spec given to setupApi).
524
+ // Don't run normalizePath on the relative form — it calls resolve() and
525
+ // would re-absolutize the path. Posix-style separators are enough for
526
+ // SQLite + Windows compat.
527
+ const dbSpecPath = localSpecAbsPath
528
+ ? relative(workspaceRoot, localSpecAbsPath).replace(/\\/g, "/")
529
+ : (openapiSpec ?? undefined);
530
+
130
531
  // Create collection in DB
131
532
  const collectionId = createCollection({
132
533
  name,
133
534
  base_dir: normalizedBaseDir,
134
535
  test_path: normalizedTestPath,
135
- openapi_spec: openapiSpec ?? undefined,
536
+ openapi_spec: dbSpecPath,
136
537
  });
137
538
 
138
539
  const pathParamsObj = pathParams.size > 0 ? Object.fromEntries(pathParams) : undefined;
@@ -145,6 +546,7 @@ export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult
145
546
  baseUrl,
146
547
  specEndpoints: endpointCount,
147
548
  ...(pathParamsObj ? { pathParams: pathParamsObj } : {}),
549
+ ...(authVarNames.length > 0 ? { authVars: authVarNames } : {}),
148
550
  ...(warnings.length > 0 ? { warnings } : {}),
149
551
  };
150
552
  }