@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,119 +0,0 @@
1
- /**
2
- * Live probe-runtime primitives shared by mass-assignment and security
3
- * probes. TASK-185 (m-11) extracted the static spec/scaffold half into
4
- * `runner.ts`; this module covers the per-endpoint primitives that
5
- * were still duplicated between the two probe entry points.
6
- *
7
- * Scope: small, pure helpers — URL building, baseline body generation,
8
- * JSON+auth header construction. Cleanup logic is intentionally NOT
9
- * unified: mass-assignment uses fire-and-forget DELETE before attacks,
10
- * security-probe uses snapshot/restore + retry-aware DELETE after
11
- * attacks. Different invariants, different shapes — see TASK-189 notes.
12
- */
13
-
14
- import type { EndpointInfo, SecuritySchemeInfo } from "../generator/types.ts";
15
- import { generateFromSchema } from "../generator/data-factory.ts";
16
- import { substituteDeep, substituteString } from "../parser/variables.ts";
17
- import { convertPath, liveAuthHeaders } from "./shared.ts";
18
- import { joinBaseAndPath } from "../util/url.ts";
19
- import { encodeFormBody } from "../runner/form-encode.ts";
20
- import type { SeedBodyConfig } from "../generator/resources-builder.ts";
21
-
22
- /** ARV-150: form-encoded mutating endpoint (Stripe v1 pattern).
23
- * Stripe and other Rails/PHP APIs declare requestBody.content with ONLY
24
- * application/x-www-form-urlencoded — the probes previously skipped
25
- * every such endpoint, masking real mass-assignment vectors. */
26
- export function isFormBody(ep: EndpointInfo): boolean {
27
- return (
28
- ep.requestBodyContentType === "application/x-www-form-urlencoded"
29
- && ep.requestBodySchema !== undefined
30
- );
31
- }
32
-
33
- /** Probes can drive either application/json or application/x-www-form-urlencoded
34
- * endpoints. Anything else (multipart, octet-stream, …) still gets skipped —
35
- * no general way to construct attack payloads without a body schema. */
36
- export function hasProbeBody(ep: EndpointInfo): boolean {
37
- if (ep.method === "GET" || ep.method === "DELETE") return false;
38
- if (!ep.requestBodySchema) return false;
39
- return (
40
- ep.requestBodyContentType === "application/json"
41
- || ep.requestBodyContentType === "application/x-www-form-urlencoded"
42
- );
43
- }
44
-
45
- /** Serialise an attack body using whichever content type the endpoint
46
- * declares. Returns the wire-format string + the Content-Type to set. */
47
- export function serializeProbeBody(
48
- ep: EndpointInfo,
49
- body: Record<string, unknown>,
50
- ): { content: string; contentType: string } {
51
- if (isFormBody(ep)) {
52
- return { content: encodeFormBody(body), contentType: "application/x-www-form-urlencoded" };
53
- }
54
- return { content: JSON.stringify(body), contentType: "application/json" };
55
- }
56
-
57
- /**
58
- * Resolve an endpoint's URL against the live `base_url` + path-param
59
- * substitutions. Returns the resolved URL and any leftover `{{var}}`
60
- * markers the caller couldn't fill — use those to skip the endpoint
61
- * with a meaningful reason.
62
- */
63
- export function buildProbeUrl(
64
- ep: EndpointInfo,
65
- vars: Record<string, string>,
66
- ): { url: string; unresolved: string[] } {
67
- const templated = joinBaseAndPath(vars["base_url"], convertPath(ep.path));
68
- const url = String(substituteString(templated, vars));
69
- const unresolved = Array.from(url.matchAll(/\{\{([^}]+)\}\}/g)).map(m => m[1]!);
70
- return { url, unresolved };
71
- }
72
-
73
- /**
74
- * Standard probe headers: content-type from the endpoint's spec
75
- * (form-urlencoded for Stripe v1, JSON otherwise) plus the resolved
76
- * auth header for the endpoint. Accept stays JSON — the server still
77
- * answers in JSON even when the body is form-encoded. Empty
78
- * `liveAuthHeaders` is fine — the spread is a no-op for unauthenticated
79
- * endpoints.
80
- */
81
- export function buildBodyAuthHeaders(
82
- ep: EndpointInfo,
83
- schemes: SecuritySchemeInfo[],
84
- vars: Record<string, string>,
85
- ): Record<string, string> {
86
- const ct = isFormBody(ep) ? "application/x-www-form-urlencoded" : "application/json";
87
- return {
88
- "content-type": ct,
89
- accept: "application/json",
90
- ...liveAuthHeaders(ep, schemes, vars),
91
- };
92
- }
93
-
94
- /**
95
- * Synthesize a baseline body from the endpoint's request schema and
96
- * substitute live vars. Returns null when the result isn't a JSON
97
- * object (array / scalar / null) — both probes treat that as a skip
98
- * reason ("request body not a JSON object").
99
- *
100
- * ARV-269: when `seedBody` is provided (agent-authored overlay from
101
- * `.api-resources.local.yaml`), it wins over `generateFromSchema`.
102
- * Strict-validating APIs (Stripe required-field XORs, `expand[]` arrays)
103
- * reject random scalars from the generator and the whole verdict becomes
104
- * INCONCLUSIVE-baseline; the overlay carries a payload the API actually
105
- * accepts. Mirrors the path stateful checks took via `resolveCreateBody`
106
- * (`core/checks/checks/_crud-helpers.ts`).
107
- */
108
- export function buildBaselineFromSpec(
109
- ep: EndpointInfo,
110
- vars: Record<string, string>,
111
- seedBody?: SeedBodyConfig,
112
- ): Record<string, unknown> | null {
113
- const raw = seedBody && seedBody.body && typeof seedBody.body === "object"
114
- ? seedBody.body
115
- : (ep.requestBodySchema ? generateFromSchema(ep.requestBodySchema) : {});
116
- const sub = substituteDeep(raw, vars);
117
- if (typeof sub !== "object" || sub === null || Array.isArray(sub)) return null;
118
- return sub as Record<string, unknown>;
119
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * Probe registry + boot-time validator (m-17 / ARV-49).
3
- *
4
- * The CLI bootstrap calls `bootstrapProbes()` exactly once — that
5
- * function imports each registered probe class and pushes it through
6
- * `registerProbe`, which throws if the contract from `types.ts` is
7
- * not fully implemented. Boot fails loud with a list of missing slots,
8
- * so adding a new probe class without --dry-run / --report support is
9
- * impossible (replaces the conventions-then-drift status quo that
10
- * produced F1-15 / F2-15 / F3-15 in feedback round 15).
11
- */
12
- import type { Probe, ProbeFlags } from "./types.ts";
13
-
14
- const REQUIRED_METHODS: Array<keyof Probe> = ["dryRun", "run", "report"];
15
- const REQUIRED_FLAGS: Array<keyof ProbeFlags> = [
16
- "api",
17
- "tag",
18
- "include",
19
- "exclude",
20
- "dryRun",
21
- "listTags",
22
- "json",
23
- "output",
24
- "report",
25
- ];
26
-
27
- export interface ValidationResult {
28
- ok: boolean;
29
- errors: string[];
30
- }
31
-
32
- /** Pure validator — used by both `registerProbe` and the contract test
33
- * in `tests/contracts/probe-interface.test.ts`. */
34
- export function validateProbe(p: unknown): ValidationResult {
35
- const errors: string[] = [];
36
- if (p === null || typeof p !== "object") {
37
- return { ok: false, errors: ["Probe is not an object"] };
38
- }
39
- const probe = p as Partial<Probe>;
40
- const label = probe.name ?? "<anonymous>";
41
- if (typeof probe.name !== "string" || probe.name.length === 0) {
42
- errors.push("Probe is missing required field name");
43
- }
44
- if (typeof probe.description !== "string" || probe.description.length === 0) {
45
- errors.push(`Probe "${label}" is missing required field description`);
46
- }
47
- for (const m of REQUIRED_METHODS) {
48
- if (typeof (probe as Record<string, unknown>)[m] !== "function") {
49
- errors.push(`Probe "${label}" is missing required method ${m}`);
50
- }
51
- }
52
- if (probe.commonFlags === undefined || probe.commonFlags === null || typeof probe.commonFlags !== "object") {
53
- errors.push(`Probe "${label}" is missing required field commonFlags`);
54
- } else {
55
- const flags = probe.commonFlags as unknown as Record<string, unknown>;
56
- for (const f of REQUIRED_FLAGS) {
57
- const v = flags[f as string];
58
- if (typeof v !== "boolean") {
59
- errors.push(`Probe "${label}" commonFlags is missing slot ${f} (must be boolean)`);
60
- }
61
- }
62
- }
63
- return { ok: errors.length === 0, errors };
64
- }
65
-
66
- const PROBES = new Map<string, Probe>();
67
-
68
- export function registerProbe(probe: Probe): void {
69
- const r = validateProbe(probe);
70
- if (!r.ok) {
71
- throw new Error(
72
- `Invalid probe registration:\n - ${r.errors.join("\n - ")}`,
73
- );
74
- }
75
- if (PROBES.has(probe.name)) {
76
- throw new Error(`Probe "${probe.name}" is already registered`);
77
- }
78
- PROBES.set(probe.name, probe);
79
- }
80
-
81
- export function listProbes(): readonly Probe[] {
82
- return Array.from(PROBES.values());
83
- }
84
-
85
- /** Test helper — wipes the registry between unit tests. NOT exported
86
- * through `index.ts`; tests import this module directly. */
87
- export function clearProbes(): void {
88
- PROBES.clear();
89
- }
@@ -1,136 +0,0 @@
1
- /**
2
- * Shared scaffolding for the four probe commands (`probe validation`,
3
- * `probe methods`, `probe mass-assignment`, `probe security`).
4
- *
5
- * Each command historically duplicated the same boilerplate: read the
6
- * spec, optionally filter by tag, mkdir the output dir, write the
7
- * generated suites with an autogen header, and record them in the
8
- * workspace manifest. This module collapses that scaffolding into two
9
- * helpers (`loadSpecForProbe` / `writeProbeSuites`) so the cli layer
10
- * stays thin.
11
- *
12
- * Live-runner commands (`mass-assignment`, `security`) reuse the
13
- * write-suites half for their `--emit-tests` flag; the actual HTTP
14
- * orchestration lives in `mass-assignment-probe.ts` /
15
- * `security-probe.ts`.
16
- *
17
- * Goals are documented in TASK-185 (m-11).
18
- */
19
-
20
- import { mkdir } from "node:fs/promises";
21
- import { join } from "node:path";
22
- import {
23
- readOpenApiSpec,
24
- extractEndpoints,
25
- extractSecuritySchemes,
26
- serializeSuite,
27
- } from "../generator/index.ts";
28
- import type { EndpointInfo, SecuritySchemeInfo, RawSuite } from "../generator/index.ts";
29
- import { collectTags, filterByTag } from "../generator/chunker.ts";
30
- import {
31
- recordGeneratedFiles,
32
- inferApiName,
33
- autoGenHeader,
34
- type RecordInput,
35
- } from "../workspace/manifest.ts";
36
- import { findWorkspaceRoot } from "../workspace/root.ts";
37
-
38
- export interface LoadSpecForProbeOptions {
39
- specPath: string;
40
- tag?: string;
41
- /** When true, the caller wants the available-tags list, not endpoints. */
42
- listTags?: boolean;
43
- }
44
-
45
- export type LoadSpecResult =
46
- | { kind: "endpoints"; endpoints: EndpointInfo[]; securitySchemes: SecuritySchemeInfo[] }
47
- | { kind: "tags"; tags: string[] }
48
- | { kind: "tag-not-found"; tag: string; available: string[] };
49
-
50
- /**
51
- * Read the spec, optionally filter by tag, and surface either the
52
- * filtered endpoints or the list of available tags. Centralises the
53
- * tag-not-found error path so probe commands all produce identical
54
- * messaging.
55
- */
56
- export async function loadSpecForProbe(opts: LoadSpecForProbeOptions): Promise<LoadSpecResult> {
57
- const doc = await readOpenApiSpec(opts.specPath);
58
- const allEndpoints = extractEndpoints(doc);
59
- const securitySchemes = extractSecuritySchemes(doc);
60
-
61
- if (opts.listTags) {
62
- return { kind: "tags", tags: collectTags(allEndpoints) };
63
- }
64
-
65
- if (opts.tag) {
66
- const filtered = filterByTag(allEndpoints, opts.tag);
67
- if (filtered.length === 0) {
68
- return { kind: "tag-not-found", tag: opts.tag, available: collectTags(allEndpoints) };
69
- }
70
- return { kind: "endpoints", endpoints: filtered, securitySchemes };
71
- }
72
-
73
- return { kind: "endpoints", endpoints: allEndpoints, securitySchemes };
74
- }
75
-
76
- export interface WriteProbeSuitesOptions {
77
- /** Directory to write suites into. Created recursively if missing. */
78
- output: string;
79
- /** Suites produced by the underlying probe generator. */
80
- suites: RawSuite[];
81
- /** Manifest `by` field — e.g. `"zond probe-methods --emit"`. */
82
- command: string;
83
- /** First arg of `autoGenHeader` (label). Defaults to `command`. */
84
- headerLabel?: string;
85
- /** Concrete repro command shown in the autogen header. */
86
- headerExample?: string;
87
- /** Manifest category (defaults to `"probes"`). */
88
- category?: RecordInput["category"];
89
- }
90
-
91
- export interface WroteProbeSuites {
92
- files: Array<{ file: string; suite: string; tests: number }>;
93
- }
94
-
95
- /**
96
- * Materialise generator output to disk and register the files in the
97
- * workspace manifest. Safe on empty input — returns an empty result and
98
- * avoids creating an empty directory (m-9 P5).
99
- */
100
- export async function writeProbeSuites(
101
- opts: WriteProbeSuitesOptions,
102
- ): Promise<WroteProbeSuites> {
103
- if (opts.suites.length === 0) return { files: [] };
104
-
105
- await mkdir(opts.output, { recursive: true });
106
-
107
- const files: WroteProbeSuites["files"] = [];
108
- const manifestEntries: RecordInput[] = [];
109
- const inferredApi = inferApiName(opts.output);
110
- const headerLabel = opts.headerLabel ?? opts.command;
111
- const headerExample = opts.headerExample ?? opts.command;
112
-
113
- for (const suite of opts.suites) {
114
- const fileName = `${suite.fileStem ?? suite.name}.yaml`;
115
- const filePath = join(opts.output, fileName);
116
- await Bun.write(filePath, autoGenHeader(headerLabel, headerExample) + serializeSuite(suite));
117
- files.push({ file: filePath, suite: suite.name, tests: suite.tests.length });
118
- manifestEntries.push({
119
- path: filePath,
120
- by: opts.command,
121
- api: inferredApi,
122
- category: opts.category ?? "probes",
123
- });
124
- }
125
-
126
- try {
127
- const ws = findWorkspaceRoot();
128
- if (!ws.fromFallback && manifestEntries.length > 0) {
129
- recordGeneratedFiles(ws.root, manifestEntries);
130
- }
131
- } catch {
132
- /* best-effort: manifest is observability, never fail probe emit on it */
133
- }
134
-
135
- return { files };
136
- }
@@ -1,174 +0,0 @@
1
- import type { EndpointInfo, SecuritySchemeInfo } from "../../generator/types.ts";
2
- import { executeRequest } from "../../runner/http-client.ts";
3
- import { findGetByIdCounterpart, liveAuthHeaders } from "../shared.ts";
4
- import { buildProbeUrl, serializeProbeBody } from "../probe-harness.ts";
5
- import type { ProbeStepOpts, SecurityVerdict } from "./types.ts";
6
-
7
- export interface Snapshot {
8
- /** Original GET-response body, used to restore state via PUT/PATCH. */
9
- body: Record<string, unknown>;
10
- /** ETag (if API uses optimistic locking) — sent back as `If-Match` on restore. */
11
- etag?: string;
12
- }
13
-
14
- export type BaselineResult =
15
- | { kind: "ok"; status: number; body: unknown; headers: Record<string, string> }
16
- | { kind: "network"; reason: string };
17
-
18
- /**
19
- * Baseline send — wraps executeRequest with shape that distinguishes a real
20
- * HTTP response from a network error (so the caller can decide whether to
21
- * retry partial-body / mark the endpoint unreachable).
22
- */
23
- export async function sendBaseline(
24
- ep: EndpointInfo,
25
- method: string,
26
- url: string,
27
- headers: Record<string, string>,
28
- body: unknown,
29
- opts: ProbeStepOpts,
30
- ): Promise<BaselineResult> {
31
- try {
32
- // ARV-161: serialize via serializeProbeBody so form-encoded endpoints
33
- // get x-www-form-urlencoded payload matching Content-Type.
34
- const wire = body && typeof body === "object" && !Array.isArray(body)
35
- ? serializeProbeBody(ep, body as Record<string, unknown>).content
36
- : JSON.stringify(body);
37
- const resp = await executeRequest(
38
- { method, url, headers, body: wire },
39
- { timeout: opts.timeoutMs ?? 30000, retries: 0 },
40
- );
41
- return {
42
- kind: "ok",
43
- status: resp.status,
44
- body: resp.body_parsed ?? resp.body,
45
- headers: resp.headers ?? {},
46
- };
47
- } catch (err) {
48
- return {
49
- kind: "network",
50
- reason: err instanceof Error ? err.message : String(err),
51
- };
52
- }
53
- }
54
-
55
- /**
56
- * TASK-151: snapshot original state via GET-by-id counterpart so PUT/PATCH
57
- * attacks can be undone. Returns null when there's no usable counterpart
58
- * or the response isn't a JSON object.
59
- */
60
- export async function snapshotOriginal(
61
- ep: EndpointInfo,
62
- allEndpoints: EndpointInfo[],
63
- schemes: SecuritySchemeInfo[],
64
- vars: Record<string, string>,
65
- opts: ProbeStepOpts,
66
- ): Promise<Snapshot | null> {
67
- const getEp = findGetByIdCounterpart(ep, allEndpoints);
68
- if (!getEp) return null;
69
- const { url, unresolved } = buildProbeUrl(getEp, vars);
70
- if (unresolved.length > 0) return null;
71
- const reqHeaders: Record<string, string> = {
72
- accept: "application/json",
73
- ...liveAuthHeaders(getEp, schemes, vars),
74
- };
75
- let resp;
76
- try {
77
- resp = await executeRequest(
78
- { method: "GET", url, headers: reqHeaders },
79
- { timeout: opts.timeoutMs ?? 30000, retries: 0 },
80
- );
81
- } catch {
82
- return null;
83
- }
84
- if (resp.status < 200 || resp.status >= 300) return null;
85
- const body = resp.body_parsed ?? resp.body;
86
- if (!body || typeof body !== "object" || Array.isArray(body)) return null;
87
-
88
- const respHeaders = resp.headers ?? {};
89
- const etag =
90
- respHeaders["etag"] ??
91
- respHeaders["ETag"] ??
92
- respHeaders["Etag"];
93
-
94
- return {
95
- body: body as Record<string, unknown>,
96
- etag: typeof etag === "string" ? etag : undefined,
97
- };
98
- }
99
-
100
- /**
101
- * Restore the original state captured by `snapshotOriginal`. Sends a
102
- * minimal PUT/PATCH containing only the fields the probe mutated —
103
- * sending the full snapshot body trips `422 use partial PUT` on
104
- * SaaS-shaped APIs (round-4 regression), so we replay each
105
- * dirty field as its own single-key request.
106
- *
107
- * `verdict.cleanup.error` is **accumulated** across calls (not
108
- * overwritten) so a single restore failure during the run is still
109
- * visible in the digest.
110
- */
111
- export async function restoreOriginal(
112
- ep: EndpointInfo,
113
- snapshot: Snapshot,
114
- baseHeaders: Record<string, string>,
115
- _schemes: SecuritySchemeInfo[],
116
- vars: Record<string, string>,
117
- opts: ProbeStepOpts,
118
- verdict: SecurityVerdict,
119
- dirtyFields: Iterable<string>,
120
- ): Promise<void> {
121
- const m = ep.method.toUpperCase();
122
- const { url, unresolved } = buildProbeUrl(ep, vars);
123
- if (unresolved.length > 0) return;
124
- const headers: Record<string, string> = { ...baseHeaders };
125
- if (snapshot.etag && ep.requiresEtag) {
126
- headers["If-Match"] = snapshot.etag;
127
- }
128
- // Filter out fields the API will reject as read-only.
129
- const READ_ONLY = new Set([
130
- "id", "created_at", "createdAt", "updated_at", "updatedAt",
131
- ]);
132
- const fields = Array.from(new Set(Array.from(dirtyFields))).filter(
133
- f => !READ_ONLY.has(f) && f in snapshot.body,
134
- );
135
-
136
- // Per-field PUT — works for both partial-PUT APIs and
137
- // full-PUT APIs (the body just carries one of the legal keys).
138
- const failures: string[] = [];
139
- let lastSuccessStatus = 0;
140
- let attempted = false;
141
- for (const field of fields) {
142
- attempted = true;
143
- const body: Record<string, unknown> = { [field]: snapshot.body[field] };
144
- let resp;
145
- try {
146
- resp = await executeRequest(
147
- { method: m, url, headers, body: serializeProbeBody(ep, body).content },
148
- { timeout: opts.timeoutMs ?? 30000, retries: 0 },
149
- );
150
- } catch (err) {
151
- failures.push(
152
- `restore.${field} network error: ${err instanceof Error ? err.message : String(err)}`,
153
- );
154
- continue;
155
- }
156
- if (resp.status < 200 || resp.status >= 300) {
157
- failures.push(`restore.${field} failed: ${resp.status}`);
158
- continue;
159
- }
160
- lastSuccessStatus = resp.status;
161
- }
162
-
163
- // Merge with any prior cleanup state on this verdict.
164
- const prior = verdict.cleanup ?? { attempted: false };
165
- const allErrors = [
166
- ...(prior.error ? [prior.error] : []),
167
- ...failures,
168
- ];
169
- verdict.cleanup = {
170
- attempted: attempted || prior.attempted,
171
- ...(lastSuccessStatus ? { status: lastSuccessStatus } : prior.status ? { status: prior.status } : {}),
172
- ...(allErrors.length > 0 ? { error: allErrors.join(" | ") } : {}),
173
- };
174
- }