@kirrosh/zond 0.26.1 → 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 +47 -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 -1162
  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,188 +0,0 @@
1
- /**
2
- * TASK-278: persist «probe created this resource and DELETE failed» records to
3
- * `~/.zond/orphans/<api>/<run-id>.jsonl` so `zond cleanup --orphans` can
4
- * retry the deletion later — without re-running the probe.
5
- *
6
- * One JSON object per line for forward-compatible streaming reads. We never
7
- * rewrite existing lines; cleanup-success appends a new `removed: true`
8
- * record that supersedes the original one (loadOrphans collapses
9
- * supersessions in memory so the on-disk file is append-only and crash-safe).
10
- */
11
- import { homedir } from "node:os";
12
- import { join } from "node:path";
13
- import { mkdir, readdir, readFile, appendFile } from "node:fs/promises";
14
- import type { SecurityVerdict } from "./security-probe.ts";
15
-
16
- export interface OrphanRecord {
17
- api: string;
18
- runId: string;
19
- /** ISO timestamp of the cleanup-attempt that produced this record. */
20
- createdAt: string;
21
- /** Method/path of the *creating* endpoint (e.g. POST /teams/). */
22
- method: string;
23
- path: string;
24
- /** Captured id of the leaked resource (slug/uuid/numeric id). */
25
- id: string;
26
- /** Concrete DELETE URL path with the id already substituted. */
27
- deletePath: string;
28
- /** Last DELETE status zond observed; `null` for network errors. */
29
- lastCleanupStatus: number | null;
30
- /** Last error string (network message or HTTP-status sentence). */
31
- lastCleanupError: string | null;
32
- /** When true, this record cancels a prior orphan with the same
33
- * (api, runId, deletePath, id) tuple. Used by `cleanup --orphans` to
34
- * mark replayed-and-now-gone resources. */
35
- removed?: boolean;
36
- /** ARV-102 (F7): probe knew the resource was created but couldn't
37
- * derive a DELETE plan (response had no usable id, or the spec has no
38
- * DELETE counterpart for the create endpoint). The record is still
39
- * worth keeping so `cleanup --orphans` can surface "manual cleanup
40
- * required" — DELETE retry isn't possible, but the user must know.
41
- * When set, deletePath / id may be empty and `lastCleanupError`
42
- * carries the reason from the probe (e.g. "cleanup skipped: response
43
- * had no usable id"). */
44
- requires_manual_cleanup?: boolean;
45
- }
46
-
47
- export function orphansRoot(): string {
48
- return process.env.ZOND_ORPHANS_DIR ?? join(homedir(), ".zond", "orphans");
49
- }
50
-
51
- function recordFile(api: string, runId: string): string {
52
- return join(orphansRoot(), api, `${runId}.jsonl`);
53
- }
54
-
55
- export async function appendOrphanRecord(record: OrphanRecord): Promise<void> {
56
- const file = recordFile(record.api, record.runId);
57
- await mkdir(join(orphansRoot(), record.api), { recursive: true });
58
- await appendFile(file, JSON.stringify(record) + "\n", "utf-8");
59
- }
60
-
61
- /**
62
- * Snapshot a probe-run's verdicts into the orphans store. We persist EVERY
63
- * cleanup attempt that has a known id (regardless of success) so a SIGINT
64
- * mid-run still leaves a trace of created resources. Successful DELETEs are
65
- * written with `lastCleanupStatus` in 2xx — `loadOrphans` will treat them as
66
- * already-removed and skip them.
67
- */
68
- export async function persistVerdictsAsOrphans(api: string, runId: string, verdicts: SecurityVerdict[]): Promise<number> {
69
- let written = 0;
70
- for (const v of verdicts) {
71
- const c = v.cleanup;
72
- if (!c?.attempted) continue;
73
- // ARV-102 (F7): pre-fix this branch dropped any verdict whose
74
- // cleanup had no usable id OR no DELETE path (e.g. "no DELETE
75
- // counterpart for POST /symbol-sources/", "response had no usable
76
- // id"). Probe digest still counted these as cleanup-failures, but
77
- // they never reached the orphan registry — `cleanup --orphans` then
78
- // reported zero, hiding live API leakage. Now we persist a
79
- // `requires_manual_cleanup: true` record so the operator at least
80
- // sees "5 resources need manual cleanup".
81
- const haveDeletePlan = c.id !== undefined && !!c.deletePath;
82
- if (!haveDeletePlan) {
83
- const record: OrphanRecord = {
84
- api,
85
- runId,
86
- createdAt: new Date().toISOString(),
87
- method: v.method.toUpperCase(),
88
- path: v.path,
89
- id: c.id !== undefined ? String(c.id) : "",
90
- deletePath: c.deletePath ?? "",
91
- lastCleanupStatus: c.status ?? null,
92
- lastCleanupError: c.error ?? "cleanup skipped: no DELETE plan",
93
- requires_manual_cleanup: true,
94
- };
95
- await appendOrphanRecord(record);
96
- written++;
97
- continue;
98
- }
99
- const removed = c.status != null && c.status >= 200 && c.status < 300;
100
- const record: OrphanRecord = {
101
- api,
102
- runId,
103
- createdAt: new Date().toISOString(),
104
- method: v.method.toUpperCase(),
105
- path: v.path,
106
- id: String(c.id),
107
- deletePath: c.deletePath ?? "",
108
- lastCleanupStatus: c.status ?? null,
109
- lastCleanupError: c.error ?? null,
110
- ...(removed ? { removed: true } : {}),
111
- };
112
- await appendOrphanRecord(record);
113
- written++;
114
- }
115
- return written;
116
- }
117
-
118
- /**
119
- * Read every orphan file (optionally filtered by `--api` and `--run`) and
120
- * return the surviving records — i.e. those NOT yet superseded by a
121
- * `removed: true` follow-up.
122
- */
123
- export async function loadOrphans(filter: { api?: string; runId?: string } = {}): Promise<OrphanRecord[]> {
124
- const root = orphansRoot();
125
- const apis: string[] = [];
126
- try {
127
- const entries = await readdir(root, { withFileTypes: true });
128
- for (const e of entries) {
129
- if (e.isDirectory() && (!filter.api || e.name === filter.api)) apis.push(e.name);
130
- }
131
- } catch {
132
- return [];
133
- }
134
-
135
- const out: OrphanRecord[] = [];
136
- for (const api of apis) {
137
- const dir = join(root, api);
138
- let files: string[] = [];
139
- try {
140
- files = (await readdir(dir)).filter(f => f.endsWith(".jsonl"));
141
- } catch {
142
- continue;
143
- }
144
- for (const f of files) {
145
- const runId = f.replace(/\.jsonl$/, "");
146
- if (filter.runId && runId !== filter.runId) continue;
147
- const file = join(dir, f);
148
- let raw: string;
149
- try {
150
- raw = await readFile(file, "utf-8");
151
- } catch {
152
- continue;
153
- }
154
- // De-dup: later records on the same (api, runId, deletePath, id) win.
155
- // `removed: true` cancels the entry; non-removed records keep the
156
- // most recent cleanup status / error.
157
- // ARV-102 (F7): manual-only records (requires_manual_cleanup) often
158
- // have empty deletePath / id (probe couldn't derive them), so the
159
- // standard key would collapse all of them onto a single bucket.
160
- // Fold method/path into the key for that branch — every distinct
161
- // (method, path) on which probe gave up survives independently.
162
- const byKey = new Map<string, OrphanRecord>();
163
- for (const line of raw.split("\n")) {
164
- const trimmed = line.trim();
165
- if (!trimmed) continue;
166
- try {
167
- const r = JSON.parse(trimmed) as OrphanRecord;
168
- const key = r.requires_manual_cleanup
169
- ? `${r.api}|${r.runId}|manual|${r.method}|${r.path}|${r.id}`
170
- : `${r.api}|${r.runId}|${r.deletePath}|${r.id}`;
171
- if (r.removed) {
172
- byKey.delete(key);
173
- } else {
174
- byKey.set(key, r);
175
- }
176
- } catch {
177
- // Skip malformed lines — best-effort parse.
178
- }
179
- }
180
- for (const r of byKey.values()) out.push(r);
181
- }
182
- }
183
- return out;
184
- }
185
-
186
- export async function markRemoved(record: OrphanRecord): Promise<void> {
187
- await appendOrphanRecord({ ...record, removed: true, lastCleanupStatus: 200, lastCleanupError: null, createdAt: new Date().toISOString() });
188
- }
@@ -1,439 +0,0 @@
1
- /**
2
- * Path-param fixture auto-discovery (TASK-92).
3
- *
4
- * Live probe runtime hook: before marking an endpoint as skipped because
5
- * `.env.yaml` doesn't supply a value for a path placeholder
6
- * (e.g. `/domains/{domain_id}` without `domain_id` in env), try to find a
7
- * sibling list endpoint (`GET /domains`), call it once per run, and extract
8
- * `data[0].id` (or compatible shape). Result cached per `GET listPath`.
9
- *
10
- * Failure modes (returned as `miss`, not exception):
11
- * • no `GET <listPath>` in spec
12
- * • list returned non-2xx
13
- * • list response has no extractable id
14
- * • list returned an empty array
15
- * • listPath itself depends on unresolved params we couldn't discover
16
- */
17
- import type { EndpointInfo, SecuritySchemeInfo } from "../generator/types.ts";
18
- import { executeRequest } from "../runner/http-client.ts";
19
- import { substituteString } from "../parser/variables.ts";
20
- import { convertPath, captureFieldFor, liveAuthHeaders } from "./shared.ts";
21
- import { joinBaseAndPath } from "../util/url.ts";
22
-
23
- export type DiscoveryHit = { kind: "hit"; values: Record<string, string> };
24
- export type DiscoveryMiss = { kind: "miss"; reason: string };
25
- export type DiscoveryResult = DiscoveryHit | DiscoveryMiss;
26
-
27
- export interface DiscoveryCache {
28
- /** key = `GET ${listPath}` (raw, with {placeholders}) */
29
- results: Map<string, DiscoveryResult>;
30
- /** keys currently being resolved — used to break cycles. */
31
- inFlight: Set<string>;
32
- }
33
-
34
- export function createDiscoveryCache(): DiscoveryCache {
35
- return { results: new Map(), inFlight: new Set() };
36
- }
37
-
38
- export interface DiscoverOptions {
39
- ep: EndpointInfo;
40
- unresolved: string[];
41
- allEndpoints: EndpointInfo[];
42
- schemes: SecuritySchemeInfo[];
43
- vars: Record<string, string>;
44
- cache: DiscoveryCache;
45
- timeoutMs?: number;
46
- }
47
-
48
- export async function discoverPathParams(opts: DiscoverOptions): Promise<DiscoveryResult> {
49
- const discovered: Record<string, string> = {};
50
- for (const name of opts.unresolved) {
51
- const listPath = parentCollectionPath(opts.ep.path, name);
52
- if (!listPath) {
53
- return { kind: "miss", reason: `cannot derive list-path for {${name}} from ${opts.ep.path}` };
54
- }
55
- const result = await discoverFromList({
56
- paramName: name,
57
- listPath,
58
- allEndpoints: opts.allEndpoints,
59
- schemes: opts.schemes,
60
- vars: { ...opts.vars, ...discovered },
61
- cache: opts.cache,
62
- timeoutMs: opts.timeoutMs,
63
- });
64
- if (result.kind === "miss") return result;
65
- Object.assign(discovered, result.values);
66
- }
67
- return { kind: "hit", values: discovered };
68
- }
69
-
70
- interface DiscoverFromListOpts {
71
- paramName: string;
72
- listPath: string;
73
- allEndpoints: EndpointInfo[];
74
- schemes: SecuritySchemeInfo[];
75
- vars: Record<string, string>;
76
- cache: DiscoveryCache;
77
- timeoutMs?: number;
78
- }
79
-
80
- async function discoverFromList(opts: DiscoverFromListOpts): Promise<DiscoveryResult> {
81
- const cacheKey = `GET ${opts.listPath}`;
82
- const cached = opts.cache.results.get(cacheKey);
83
- if (cached) {
84
- if (cached.kind === "hit") {
85
- const v = pickValue(cached.values);
86
- if (v !== undefined) return { kind: "hit", values: { [opts.paramName]: v } };
87
- return { kind: "miss", reason: `cached list ${opts.listPath} has no usable id` };
88
- }
89
- return cached;
90
- }
91
- if (opts.cache.inFlight.has(cacheKey)) {
92
- return { kind: "miss", reason: `cycle detected resolving ${opts.listPath}` };
93
- }
94
-
95
- const listEp = opts.allEndpoints.find(
96
- e => e.method.toUpperCase() === "GET" && e.path === opts.listPath && !e.deprecated,
97
- );
98
- if (!listEp) {
99
- const miss: DiscoveryMiss = { kind: "miss", reason: `no GET ${opts.listPath} in spec` };
100
- opts.cache.results.set(cacheKey, miss);
101
- return miss;
102
- }
103
-
104
- opts.cache.inFlight.add(cacheKey);
105
- try {
106
- // Resolve listEp's own path placeholders (nested-collection case).
107
- const templated = joinBaseAndPath(opts.vars["base_url"], convertPath(listEp.path));
108
- let urlVars = opts.vars;
109
- let urlSubstituted = String(substituteString(templated, urlVars));
110
- let stillUnresolved = collectUnresolved(urlSubstituted);
111
- if (stillUnresolved.length > 0) {
112
- const inner = await discoverPathParams({
113
- ep: listEp,
114
- unresolved: stillUnresolved,
115
- allEndpoints: opts.allEndpoints,
116
- schemes: opts.schemes,
117
- vars: opts.vars,
118
- cache: opts.cache,
119
- timeoutMs: opts.timeoutMs,
120
- });
121
- if (inner.kind === "miss") {
122
- const miss: DiscoveryMiss = {
123
- kind: "miss",
124
- reason: `parent of {${opts.paramName}} unresolved (${inner.reason})`,
125
- };
126
- opts.cache.results.set(cacheKey, miss);
127
- return miss;
128
- }
129
- urlVars = { ...opts.vars, ...inner.values };
130
- urlSubstituted = String(substituteString(templated, urlVars));
131
- stillUnresolved = collectUnresolved(urlSubstituted);
132
- if (stillUnresolved.length > 0) {
133
- const miss: DiscoveryMiss = {
134
- kind: "miss",
135
- reason: `parent of {${opts.paramName}} unresolved after discovery: ${stillUnresolved.join(", ")}`,
136
- };
137
- opts.cache.results.set(cacheKey, miss);
138
- return miss;
139
- }
140
- }
141
-
142
- const url = appendLimitOne(urlSubstituted, listEp);
143
- const headers: Record<string, string> = {
144
- accept: "application/json",
145
- ...liveAuthHeaders(listEp, opts.schemes, urlVars),
146
- };
147
-
148
- let resp;
149
- try {
150
- resp = await executeRequest(
151
- { method: "GET", url, headers },
152
- { timeout: opts.timeoutMs ?? 30000, retries: 0 },
153
- );
154
- } catch (err) {
155
- const miss: DiscoveryMiss = {
156
- kind: "miss",
157
- reason: `GET ${opts.listPath} network error: ${err instanceof Error ? err.message : String(err)}`,
158
- };
159
- opts.cache.results.set(cacheKey, miss);
160
- return miss;
161
- }
162
- if (resp.status < 200 || resp.status >= 300) {
163
- const miss: DiscoveryMiss = {
164
- kind: "miss",
165
- reason: `GET ${opts.listPath} returned ${resp.status}`,
166
- };
167
- opts.cache.results.set(cacheKey, miss);
168
- return miss;
169
- }
170
- const id = extractFirstId(resp.body_parsed ?? resp.body, listEp);
171
- if (id === undefined) {
172
- // Distinguish empty-list from has-items-but-no-id-shape.
173
- const empty = isEmptyList(resp.body_parsed ?? resp.body);
174
- const miss: DiscoveryMiss = {
175
- kind: "miss",
176
- reason: empty
177
- ? `auto-discovery: GET ${opts.listPath} returned empty list`
178
- : `GET ${opts.listPath} response has no extractable id`,
179
- };
180
- opts.cache.results.set(cacheKey, miss);
181
- return miss;
182
- }
183
- const hit: DiscoveryHit = { kind: "hit", values: { [opts.paramName]: id } };
184
- opts.cache.results.set(cacheKey, hit);
185
- return hit;
186
- } finally {
187
- opts.cache.inFlight.delete(cacheKey);
188
- }
189
- }
190
-
191
- /** Walk segments of `path`, return everything before the first `{paramName}` segment. */
192
- export function parentCollectionPath(path: string, paramName: string): string | undefined {
193
- const segments = path.split("/");
194
- const idx = segments.findIndex(seg => seg === `{${paramName}}`);
195
- if (idx <= 0) return undefined;
196
- return segments.slice(0, idx).join("/") || "/";
197
- }
198
-
199
- function collectUnresolved(url: string): string[] {
200
- return Array.from(url.matchAll(/\{\{([^}]+)\}\}/g)).map(m => m[1]!);
201
- }
202
-
203
- function appendLimitOne(url: string, listEp: EndpointInfo): string {
204
- const hasLimitParam = listEp.parameters.some(
205
- p => p.in === "query" && (p.name === "limit" || p.name === "per_page" || p.name === "page_size"),
206
- );
207
- if (!hasLimitParam) return url;
208
- const sep = url.includes("?") ? "&" : "?";
209
- const name = listEp.parameters.find(
210
- p => p.in === "query" && (p.name === "limit" || p.name === "per_page" || p.name === "page_size"),
211
- )!.name;
212
- return `${url}${sep}${name}=1`;
213
- }
214
-
215
- /**
216
- * Body-FK auto-discovery (TASK-137).
217
- *
218
- * Mirror of `discoverPathParams` for body-level FK fields. probe-mass-assignment
219
- * builds its baseline body from the spec; required FK fields like
220
- * `audience_id` / `project_slug` get filled with `{{$randomString}}` and the
221
- * server returns 4xx → INCONCLUSIVE-baseline. We can do better: scan the
222
- * request schema for FK-named fields, find a likely list endpoint
223
- * (`GET /audiences` for `audience_id`), call it once, cache the id.
224
- *
225
- * The map returned is **suffix-aware**: `*_slug` → `slug`, `*_uuid` → `uuid`,
226
- * else `id`. This matches what the env-var name implies — the same rule
227
- * `zond discover` (TASK-136) uses for its CLI flow.
228
- */
229
- const BODY_FK_SUFFIXES = ["_id", "_uuid", "_slug", "_key"] as const;
230
-
231
- interface BodyFkField {
232
- /** Variable / body-field name (e.g. `audience_id`). */
233
- name: string;
234
- /** Resource stem (e.g. `audience`). */
235
- stem: string;
236
- /** Field expected on response item (`id`/`slug`/`uuid`/`key`). */
237
- preferredField: string;
238
- }
239
-
240
- function collectBodyFkFields(
241
- schema: import("openapi-types").OpenAPIV3.SchemaObject | undefined,
242
- ): BodyFkField[] {
243
- if (!schema || !schema.properties) return [];
244
- const required = new Set(schema.required ?? []);
245
- const out: BodyFkField[] = [];
246
- for (const name of Object.keys(schema.properties)) {
247
- if (!required.has(name)) continue;
248
- for (const suffix of BODY_FK_SUFFIXES) {
249
- if (name.endsWith(suffix)) {
250
- const stem = name.slice(0, -suffix.length);
251
- if (!stem) break;
252
- const preferredField = suffix === "_id" ? "id" : suffix.slice(1);
253
- out.push({ name, stem, preferredField });
254
- break;
255
- }
256
- }
257
- }
258
- return out;
259
- }
260
-
261
- /** Walk the spec for a likely list endpoint owning the given stem. We accept
262
- * collection-level GET on `/<stem>s`, `/<stem>`, or any path ending in those.
263
- * Skips path-templated lists (need parent context). */
264
- function findOwnerListEndpoint(
265
- stem: string,
266
- endpoints: EndpointInfo[],
267
- ): EndpointInfo | undefined {
268
- const candidates = [`${stem}s`, stem, stem.replace(/y$/, "ies"), stem.replace(/s$/, "")];
269
- for (const candidate of candidates) {
270
- if (!candidate) continue;
271
- const re = new RegExp(`/${candidate}/?$`, "i");
272
- const ep = endpoints.find(
273
- e =>
274
- e.method.toUpperCase() === "GET" &&
275
- !e.deprecated &&
276
- !e.path.includes("{") &&
277
- re.test(e.path),
278
- );
279
- if (ep) return ep;
280
- }
281
- return undefined;
282
- }
283
-
284
- export interface BodyFkDiscoveryOptions {
285
- ep: EndpointInfo;
286
- allEndpoints: EndpointInfo[];
287
- schemes: SecuritySchemeInfo[];
288
- vars: Record<string, string>;
289
- cache: DiscoveryCache;
290
- timeoutMs?: number;
291
- }
292
-
293
- export interface BodyFkDiscoveryResult {
294
- /** Discovered values to merge into vars (only when missing in vars). */
295
- values: Record<string, string>;
296
- /** Field names that look like FK but couldn't be resolved (with reason). */
297
- misses: Array<{ field: string; reason: string }>;
298
- }
299
-
300
- export async function discoverBodyFkVars(
301
- opts: BodyFkDiscoveryOptions,
302
- ): Promise<BodyFkDiscoveryResult> {
303
- const fields = collectBodyFkFields(opts.ep.requestBodySchema);
304
- const values: Record<string, string> = {};
305
- const misses: Array<{ field: string; reason: string }> = [];
306
-
307
- for (const field of fields) {
308
- if (opts.vars[field.name] !== undefined && opts.vars[field.name] !== "") {
309
- // already supplied by the user — nothing to do.
310
- continue;
311
- }
312
- const listEp = findOwnerListEndpoint(field.stem, opts.allEndpoints);
313
- if (!listEp) {
314
- misses.push({ field: field.name, reason: `no GET /<${field.stem}s> in spec` });
315
- continue;
316
- }
317
-
318
- const cacheKey = `GET ${listEp.path}#${field.preferredField}`;
319
- const cached = opts.cache.results.get(cacheKey);
320
- if (cached) {
321
- if (cached.kind === "hit" && cached.values[field.name] !== undefined) {
322
- values[field.name] = cached.values[field.name]!;
323
- } else if (cached.kind === "miss") {
324
- misses.push({ field: field.name, reason: cached.reason });
325
- }
326
- continue;
327
- }
328
-
329
- const url = joinBaseAndPath(opts.vars["base_url"], listEp.path);
330
- const headers: Record<string, string> = {
331
- accept: "application/json",
332
- ...liveAuthHeaders(listEp, opts.schemes, opts.vars),
333
- };
334
-
335
- let resp;
336
- try {
337
- resp = await executeRequest(
338
- { method: "GET", url, headers },
339
- { timeout: opts.timeoutMs ?? 30000, retries: 0 },
340
- );
341
- } catch (err) {
342
- const reason = `network error on GET ${listEp.path}: ${err instanceof Error ? err.message : String(err)}`;
343
- opts.cache.results.set(cacheKey, { kind: "miss", reason });
344
- misses.push({ field: field.name, reason });
345
- continue;
346
- }
347
- if (resp.status < 200 || resp.status >= 300) {
348
- const reason = `GET ${listEp.path} → ${resp.status}`;
349
- opts.cache.results.set(cacheKey, { kind: "miss", reason });
350
- misses.push({ field: field.name, reason });
351
- continue;
352
- }
353
- const id = pickFieldFromBody(
354
- resp.body_parsed ?? resp.body,
355
- field.preferredField,
356
- );
357
- if (id === undefined) {
358
- const reason = `GET ${listEp.path} response has no usable ${field.preferredField} field`;
359
- opts.cache.results.set(cacheKey, { kind: "miss", reason });
360
- misses.push({ field: field.name, reason });
361
- continue;
362
- }
363
- opts.cache.results.set(cacheKey, { kind: "hit", values: { [field.name]: id } });
364
- values[field.name] = id;
365
- }
366
-
367
- return { values, misses };
368
- }
369
-
370
- function pickFieldFromBody(body: unknown, preferred: string): string | undefined {
371
- const tryItem = (item: unknown): string | undefined => {
372
- if (!item || typeof item !== "object") return undefined;
373
- const obj = item as Record<string, unknown>;
374
- for (const key of [preferred, "id", "slug", "uuid", "key"]) {
375
- if (key in obj) {
376
- const v = obj[key];
377
- if (typeof v === "string" || typeof v === "number") return String(v);
378
- }
379
- }
380
- return undefined;
381
- };
382
- if (Array.isArray(body)) return tryItem(body[0]);
383
- if (body && typeof body === "object") {
384
- const obj = body as Record<string, unknown>;
385
- for (const wrap of ["data", "items", "results", "records"]) {
386
- const arr = obj[wrap];
387
- if (Array.isArray(arr) && arr.length > 0) return tryItem(arr[0]);
388
- }
389
- }
390
- return undefined;
391
- }
392
-
393
- /** Try several common SaaS list-response shapes. */
394
- function extractFirstId(body: unknown, listEp: EndpointInfo): string | undefined {
395
- if (Array.isArray(body)) {
396
- return idFromItem(body[0], listEp);
397
- }
398
- if (body && typeof body === "object") {
399
- const obj = body as Record<string, unknown>;
400
- for (const key of ["data", "items", "results", "records"]) {
401
- const arr = obj[key];
402
- if (Array.isArray(arr) && arr.length > 0) {
403
- return idFromItem(arr[0], listEp);
404
- }
405
- }
406
- }
407
- return undefined;
408
- }
409
-
410
- function idFromItem(item: unknown, listEp: EndpointInfo): string | undefined {
411
- if (!item || typeof item !== "object") return undefined;
412
- const obj = item as Record<string, unknown>;
413
- // Prefer "id"; fall back to first uuid-shaped field hinted by spec.
414
- if (typeof obj["id"] === "string" || typeof obj["id"] === "number") {
415
- return String(obj["id"]);
416
- }
417
- const hinted = captureFieldFor(listEp);
418
- if (hinted in obj && (typeof obj[hinted] === "string" || typeof obj[hinted] === "number")) {
419
- return String(obj[hinted]);
420
- }
421
- return undefined;
422
- }
423
-
424
- function isEmptyList(body: unknown): boolean {
425
- if (Array.isArray(body)) return body.length === 0;
426
- if (body && typeof body === "object") {
427
- const obj = body as Record<string, unknown>;
428
- for (const key of ["data", "items", "results", "records"]) {
429
- const arr = obj[key];
430
- if (Array.isArray(arr)) return arr.length === 0;
431
- }
432
- }
433
- return false;
434
- }
435
-
436
- function pickValue(values: Record<string, string>): string | undefined {
437
- for (const v of Object.values(values)) if (typeof v === "string") return v;
438
- return undefined;
439
- }