@kirrosh/zond 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/CHANGELOG.md +648 -0
  2. package/README.md +58 -6
  3. package/package.json +9 -6
  4. package/src/cli/argv.ts +122 -0
  5. package/src/cli/commands/add-api.ts +134 -0
  6. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  7. package/src/cli/commands/api/annotate/index.ts +525 -0
  8. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  9. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  10. package/src/cli/commands/api/annotate/pagination.ts +60 -0
  11. package/src/cli/commands/api/annotate/prompts.ts +183 -0
  12. package/src/cli/commands/api/annotate/readback.ts +58 -0
  13. package/src/cli/commands/api/annotate/resources.ts +91 -0
  14. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  15. package/src/cli/commands/audit.ts +480 -0
  16. package/src/cli/commands/bootstrap.ts +710 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +348 -0
  19. package/src/cli/commands/checks.ts +756 -0
  20. package/src/cli/commands/ci-init.ts +43 -0
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +262 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +178 -7
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1236 -0
  28. package/src/cli/commands/doctor.ts +607 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +420 -46
  31. package/src/cli/commands/init/bootstrap.ts +30 -1
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +56 -3
  34. package/src/cli/commands/init/templates/agents.md +65 -61
  35. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  36. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  37. package/src/cli/commands/init/templates/skills/zond.md +592 -125
  38. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  39. package/src/cli/commands/prepare-fixtures.ts +135 -0
  40. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  41. package/src/cli/commands/probe/security.ts +454 -0
  42. package/src/cli/commands/probe/static.ts +255 -0
  43. package/src/cli/commands/probe/webhooks.ts +161 -0
  44. package/src/cli/commands/probe.ts +459 -0
  45. package/src/cli/commands/reference.ts +87 -0
  46. package/src/cli/commands/refresh-api.ts +169 -0
  47. package/src/cli/commands/remove-api.ts +150 -0
  48. package/src/cli/commands/report-bundle.ts +318 -0
  49. package/src/cli/commands/report.ts +241 -0
  50. package/src/cli/commands/request.ts +379 -4
  51. package/src/cli/commands/run.ts +842 -53
  52. package/src/cli/commands/session.ts +244 -0
  53. package/src/cli/commands/use.ts +18 -1
  54. package/src/cli/index.ts +20 -3
  55. package/src/cli/json-envelope.ts +112 -3
  56. package/src/cli/json-schemas.ts +263 -0
  57. package/src/cli/program.ts +198 -635
  58. package/src/cli/resolve.ts +105 -0
  59. package/src/cli/status-filter.ts +124 -0
  60. package/src/cli/util/api-context.ts +85 -0
  61. package/src/cli/version.ts +5 -0
  62. package/src/core/anti-fp/bootstrap.ts +34 -0
  63. package/src/core/anti-fp/index.ts +33 -0
  64. package/src/core/anti-fp/registry.ts +44 -0
  65. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  66. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  67. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  68. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  69. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  70. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  71. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  72. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  73. package/src/core/anti-fp/types.ts +68 -0
  74. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  75. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  76. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  77. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  78. package/src/core/checks/checks/cross_call_references.ts +134 -0
  79. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  80. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  81. package/src/core/checks/checks/ignored_auth.ts +211 -0
  82. package/src/core/checks/checks/index.ts +65 -0
  83. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  84. package/src/core/checks/checks/missing_required_header.ts +40 -0
  85. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  86. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  87. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  88. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  89. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  90. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  91. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  92. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  93. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  94. package/src/core/checks/checks/unsupported_method.ts +63 -0
  95. package/src/core/checks/checks/use_after_free.ts +78 -0
  96. package/src/core/checks/index.ts +30 -0
  97. package/src/core/checks/mode.ts +79 -0
  98. package/src/core/checks/recommended-action.ts +64 -0
  99. package/src/core/checks/registry.ts +78 -0
  100. package/src/core/checks/runner.ts +874 -0
  101. package/src/core/checks/sarif.ts +230 -0
  102. package/src/core/checks/stateful.ts +121 -0
  103. package/src/core/checks/types.ts +189 -0
  104. package/src/core/classifier/recommended-action.ts +222 -0
  105. package/src/core/context/current.ts +22 -6
  106. package/src/core/context/session.ts +78 -0
  107. package/src/core/coverage/loader.ts +185 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +151 -11
  110. package/src/core/diagnostics/failure-class.ts +120 -0
  111. package/src/core/diagnostics/failure-hints.ts +212 -9
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  114. package/src/core/exporter/case-study/index.ts +270 -0
  115. package/src/core/exporter/curl.ts +40 -0
  116. package/src/core/exporter/exporter.ts +48 -0
  117. package/src/core/exporter/html-report/escape.ts +24 -0
  118. package/src/core/exporter/html-report/index.ts +479 -0
  119. package/src/core/exporter/html-report/script.ts +100 -0
  120. package/src/core/exporter/html-report/styles.ts +408 -0
  121. package/src/core/generator/chunker.ts +42 -16
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/create-body.ts +89 -0
  124. package/src/core/generator/data-factory.ts +445 -19
  125. package/src/core/generator/describe.ts +1 -1
  126. package/src/core/generator/fixtures-builder.ts +325 -0
  127. package/src/core/generator/index.ts +7 -5
  128. package/src/core/generator/openapi-reader.ts +37 -3
  129. package/src/core/generator/path-param-disambig.ts +114 -0
  130. package/src/core/generator/resources-builder.ts +648 -0
  131. package/src/core/generator/schema-utils.ts +11 -3
  132. package/src/core/generator/serializer.ts +103 -13
  133. package/src/core/generator/suite-generator.ts +419 -111
  134. package/src/core/generator/types.ts +8 -0
  135. package/src/core/identity/identity-file.ts +129 -0
  136. package/src/core/lint/affects.ts +28 -0
  137. package/src/core/lint/config.ts +96 -0
  138. package/src/core/lint/format.ts +42 -0
  139. package/src/core/lint/index.ts +94 -0
  140. package/src/core/lint/reporter.ts +128 -0
  141. package/src/core/lint/rules/consistency.ts +158 -0
  142. package/src/core/lint/rules/heuristics.ts +97 -0
  143. package/src/core/lint/rules/strictness.ts +109 -0
  144. package/src/core/lint/types.ts +96 -0
  145. package/src/core/lint/walker.ts +248 -0
  146. package/src/core/meta/meta-store.ts +6 -73
  147. package/src/core/output/README.md +91 -0
  148. package/src/core/output/index.ts +13 -0
  149. package/src/core/output/run.ts +126 -0
  150. package/src/core/output/types.ts +129 -0
  151. package/src/core/parser/env-interpolation.ts +104 -0
  152. package/src/core/parser/filter.ts +57 -0
  153. package/src/core/parser/schema.ts +129 -4
  154. package/src/core/parser/types.ts +19 -1
  155. package/src/core/parser/variables.ts +0 -0
  156. package/src/core/parser/yaml-parser.ts +58 -12
  157. package/src/core/probe/bootstrap.ts +34 -0
  158. package/src/core/probe/dry-run-envelope.ts +57 -0
  159. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  160. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  161. package/src/core/probe/mass-assignment-template.ts +212 -0
  162. package/src/core/probe/method-probe.ts +43 -76
  163. package/src/core/probe/method-shared.ts +69 -0
  164. package/src/core/probe/negative-probe.ts +183 -149
  165. package/src/core/probe/orphan-tracker.ts +188 -0
  166. package/src/core/probe/path-discovery.ts +440 -0
  167. package/src/core/probe/probe-harness.ts +120 -0
  168. package/src/core/probe/registry.ts +89 -0
  169. package/src/core/probe/runner.ts +136 -0
  170. package/src/core/probe/security-probe-class.ts +201 -0
  171. package/src/core/probe/security-probe.ts +1453 -0
  172. package/src/core/probe/shared.ts +505 -0
  173. package/src/core/probe/static-probe-class.ts +125 -0
  174. package/src/core/probe/types.ts +165 -0
  175. package/src/core/probe/verdict-aggregator.ts +33 -0
  176. package/src/core/probe/webhooks-probe.ts +284 -0
  177. package/src/core/reporter/console.ts +41 -2
  178. package/src/core/reporter/index.ts +2 -3
  179. package/src/core/reporter/json.ts +11 -1
  180. package/src/core/reporter/junit.ts +27 -12
  181. package/src/core/reporter/ndjson.ts +37 -0
  182. package/src/core/reporter/types.ts +3 -0
  183. package/src/core/runner/assertions.ts +58 -1
  184. package/src/core/runner/async-pool.ts +108 -0
  185. package/src/core/runner/auth-path.ts +8 -0
  186. package/src/core/runner/ci-context.ts +72 -0
  187. package/src/core/runner/executor.ts +264 -20
  188. package/src/core/runner/form-encode.ts +51 -0
  189. package/src/core/runner/http-client.ts +75 -2
  190. package/src/core/runner/learn-drift.ts +293 -0
  191. package/src/core/runner/preflight-vars.ts +149 -0
  192. package/src/core/runner/progress-tracker.ts +73 -0
  193. package/src/core/runner/rate-limiter.ts +89 -17
  194. package/src/core/runner/run-kind.ts +39 -0
  195. package/src/core/runner/schema-validator.ts +312 -0
  196. package/src/core/runner/send-request.ts +153 -20
  197. package/src/core/runner/types.ts +38 -0
  198. package/src/core/secrets/registry.ts +164 -0
  199. package/src/core/secrets/secrets-file.ts +115 -0
  200. package/src/core/selectors/operation-filter.ts +144 -0
  201. package/src/core/setup-api.ts +415 -16
  202. package/src/core/severity/category.ts +94 -0
  203. package/src/core/severity/index.ts +121 -0
  204. package/src/core/spec/layers.ts +154 -0
  205. package/src/core/util/format-eta.ts +21 -0
  206. package/src/core/utils.ts +5 -1
  207. package/src/core/workspace/config.ts +129 -0
  208. package/src/core/workspace/manifest.ts +283 -0
  209. package/src/core/workspace/output-rotation.ts +62 -0
  210. package/src/core/workspace/triage-path.ts +87 -0
  211. package/src/db/lint-runs.ts +47 -0
  212. package/src/db/migrate.ts +126 -0
  213. package/src/db/migrations/0001_run_kind.sql +25 -0
  214. package/src/db/migrations/sql.d.ts +4 -0
  215. package/src/db/queries/collections.ts +133 -0
  216. package/src/db/queries/coverage.ts +9 -0
  217. package/src/db/queries/dashboard.ts +59 -0
  218. package/src/db/queries/results.ts +128 -0
  219. package/src/db/queries/runs.ts +235 -0
  220. package/src/db/queries/sessions.ts +42 -0
  221. package/src/db/queries/settings.ts +28 -0
  222. package/src/db/queries/types.ts +172 -0
  223. package/src/db/queries.ts +72 -802
  224. package/src/db/schema.ts +178 -50
  225. package/src/cli/commands/export.ts +0 -144
  226. package/src/cli/commands/guide.ts +0 -127
  227. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  228. package/src/cli/commands/probe-methods.ts +0 -108
  229. package/src/cli/commands/probe-validation.ts +0 -124
  230. package/src/cli/commands/serve.ts +0 -114
  231. package/src/cli/commands/sync.ts +0 -268
  232. package/src/cli/commands/update.ts +0 -189
  233. package/src/cli/commands/validate.ts +0 -34
  234. package/src/core/diagnostics/render-md.ts +0 -112
  235. package/src/core/exporter/postman.ts +0 -963
  236. package/src/core/generator/guide-builder.ts +0 -253
  237. package/src/core/meta/types.ts +0 -19
  238. package/src/core/parser/index.ts +0 -21
  239. package/src/core/runner/execute-run.ts +0 -132
  240. package/src/core/runner/index.ts +0 -12
  241. package/src/core/sync/spec-differ.ts +0 -38
  242. package/src/web/data/collection-state.ts +0 -362
  243. package/src/web/routes/api.ts +0 -314
  244. package/src/web/routes/dashboard.ts +0 -350
  245. package/src/web/routes/runs.ts +0 -64
  246. package/src/web/schemas.ts +0 -121
  247. package/src/web/server.ts +0 -134
  248. package/src/web/static/htmx.min.cjs +0 -1
  249. package/src/web/static/style.css +0 -1148
  250. package/src/web/views/endpoints-tab.ts +0 -174
  251. package/src/web/views/explorer-tab.ts +0 -402
  252. package/src/web/views/health-strip.ts +0 -92
  253. package/src/web/views/layout.ts +0 -48
  254. package/src/web/views/results.ts +0 -210
  255. package/src/web/views/runs-tab.ts +0 -126
  256. package/src/web/views/suites-tab.ts +0 -181
@@ -0,0 +1,164 @@
1
+ /**
2
+ * SecretRegistry — runtime registry of secret values + sanitizer
3
+ * (TASK-166, m-10).
4
+ *
5
+ * The registry is the single point that knows "this string is a secret;
6
+ * if you see it anywhere in a request URL, body, response, log line, or
7
+ * exporter, replace it with `<redacted:<var-name>>`". Every persisted
8
+ * artifact path (DB-write — TASK-167, exporters — TASK-168) calls
9
+ * `redact()` / `redactObject()` before writing, so the user can ship a
10
+ * digest / HTML report without scrubbing tokens by hand.
11
+ *
12
+ * Design rules:
13
+ * - Exact-match only — no heuristics ("looks like a JWT", "starts with
14
+ * `sk_`"). False positives are worse than false negatives here.
15
+ * - Minimum length 8 — protects against `auth_token: ""` or `id: 1`
16
+ * turning every "1" in the report into `<redacted>`.
17
+ * - One marker format documented in one place: `<redacted:<name>>`.
18
+ * - `setEnabled(false)` returns a no-op redactor for `--no-redact` (local
19
+ * debug). Default is enabled.
20
+ *
21
+ * Marker format: `<redacted:auth_token>` — the name comes from the
22
+ * variable that registered the value (e.g. `.env.yaml` key, `--env` flag,
23
+ * `.secrets.yaml` future entry). Anything that opens a redacted artifact
24
+ * sees the variable name and knows where to look it up locally.
25
+ */
26
+
27
+ /** Minimum length below which a registered value is silently ignored. */
28
+ export const MIN_SECRET_LENGTH = 8;
29
+
30
+ const REDACTED_MARKER_RE = /<redacted:[a-zA-Z0-9_.-]+>/;
31
+
32
+ export interface SecretEntry {
33
+ name: string;
34
+ value: string;
35
+ }
36
+
37
+ export class SecretRegistry {
38
+ /** value → name. We keep map keyed by *value* so a single redact pass
39
+ * iterates the unique values rather than all registrations. Two names
40
+ * registering the same value collapse to one entry — the most recent
41
+ * wins. */
42
+ private byValue = new Map<string, string>();
43
+ private enabled = true;
44
+
45
+ register(name: string, value: unknown): void {
46
+ if (typeof value !== "string") return;
47
+ if (value.length < MIN_SECRET_LENGTH) return;
48
+ this.byValue.set(value, name);
49
+ }
50
+
51
+ /**
52
+ * Bulk-register every string value in a flat object. Used by
53
+ * `.env.yaml` / `.secrets.yaml` loaders so we don't have to know in
54
+ * advance which keys are sensitive. The variable name carried into the
55
+ * marker is the object key.
56
+ */
57
+ registerAll(entries: Record<string, unknown> | undefined | null): void {
58
+ if (!entries) return;
59
+ for (const [k, v] of Object.entries(entries)) this.register(k, v);
60
+ }
61
+
62
+ /** Disable redaction (for `--no-redact` local debug). */
63
+ setEnabled(enabled: boolean): void {
64
+ this.enabled = enabled;
65
+ }
66
+
67
+ isEnabled(): boolean {
68
+ return this.enabled;
69
+ }
70
+
71
+ /** Names of every var that had a value registered. Stable diagnostic. */
72
+ redactedNames(): string[] {
73
+ return [...new Set(this.byValue.values())].sort();
74
+ }
75
+
76
+ hasSecrets(): boolean {
77
+ return this.byValue.size > 0;
78
+ }
79
+
80
+ /** Drop all registered secrets — used between test cases. */
81
+ clear(): void {
82
+ this.byValue.clear();
83
+ }
84
+
85
+ /**
86
+ * Replace every occurrence of a registered value in `text` with the
87
+ * marker `<redacted:<name>>`. Longest values first, so a token that
88
+ * happens to contain a shorter registered substring still ends up
89
+ * redacted as the more-specific match.
90
+ */
91
+ redact(text: string): string {
92
+ if (!this.enabled || this.byValue.size === 0) return text;
93
+ if (typeof text !== "string" || text.length === 0) return text;
94
+
95
+ let out = text;
96
+ for (const [value, name] of this.sortedEntries()) {
97
+ if (out.indexOf(value) === -1) continue;
98
+ out = out.split(value).join(`<redacted:${name}>`);
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * Deep-clone variant for arbitrary structured data (request/response
105
+ * bodies, header maps, JSON envelopes). Strings get redacted; numbers,
106
+ * booleans, null, Buffers stay as-is. Cycles are not expected on the
107
+ * artifact paths but we guard with `seen` to be safe.
108
+ */
109
+ redactObject<T>(obj: T): T {
110
+ if (!this.enabled || this.byValue.size === 0) return obj;
111
+ return this.deepRedact(obj, new WeakSet()) as T;
112
+ }
113
+
114
+ private sortedEntries(): Array<[string, string]> {
115
+ return [...this.byValue.entries()].sort((a, b) => b[0].length - a[0].length);
116
+ }
117
+
118
+ private deepRedact(node: unknown, seen: WeakSet<object>): unknown {
119
+ if (node == null) return node;
120
+ if (typeof node === "string") return this.redact(node);
121
+ if (typeof node !== "object") return node;
122
+ if (seen.has(node as object)) return node;
123
+ seen.add(node as object);
124
+
125
+ if (Array.isArray(node)) {
126
+ return node.map((v) => this.deepRedact(v, seen));
127
+ }
128
+ // Buffers / Uint8Array / Date — leave intact.
129
+ if (node instanceof Uint8Array || node instanceof Date) return node;
130
+
131
+ const out: Record<string, unknown> = {};
132
+ for (const [k, v] of Object.entries(node)) {
133
+ out[k] = this.deepRedact(v, seen);
134
+ }
135
+ return out;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Process-wide registry. CLI commands populate it once after loading
141
+ * `.env.yaml` / `.secrets.yaml`; library callers can pass their own
142
+ * instance instead of touching this singleton in tests.
143
+ */
144
+ let globalRegistry: SecretRegistry | undefined;
145
+
146
+ export function getSecretRegistry(): SecretRegistry {
147
+ if (!globalRegistry) globalRegistry = new SecretRegistry();
148
+ return globalRegistry;
149
+ }
150
+
151
+ /** Replace the global registry. Tests use this to reset state. */
152
+ export function setSecretRegistry(reg: SecretRegistry): void {
153
+ globalRegistry = reg;
154
+ }
155
+
156
+ /** Convenience: redact a string via the global registry. */
157
+ export function redact(text: string): string {
158
+ return getSecretRegistry().redact(text);
159
+ }
160
+
161
+ /** Convenience: redact a nested object via the global registry. */
162
+ export function redactObject<T>(value: T): T {
163
+ return getSecretRegistry().redactObject(value);
164
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * `.secrets.yaml` — gitignored flat YAML file holding raw secret values
3
+ * for an API (TASK-170, m-10). Companion to `.env.yaml` which references
4
+ * keys here via `@secret:<name>`.
5
+ *
6
+ * # apis/<name>/.secrets.yaml (NEVER committed)
7
+ * auth_token: "tok_..."
8
+ * dsn: "https://...@example.com/..."
9
+ *
10
+ * # apis/<name>/.env.yaml (committable)
11
+ * auth_token: "@secret:auth_token"
12
+ * base_url: "https://api.example.com"
13
+ *
14
+ * Mental model: anything in `.secrets.yaml` is registered with the
15
+ * `SecretRegistry` at load-time, so it gets redacted in any persisted
16
+ * artifact (DB, exporters, digests). Anything in `.env.yaml` is plain.
17
+ */
18
+
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+ import { getSecretRegistry } from "./registry.ts";
22
+
23
+ const SECRETS_FILENAME = ".secrets.yaml";
24
+ const SECRET_REF_RE = /^@secret:([A-Za-z_][A-Za-z0-9_.-]*)$/;
25
+
26
+ /** Resolved contents of a `.secrets.yaml`. */
27
+ export interface SecretsFile {
28
+ filePath: string;
29
+ values: Record<string, string>;
30
+ }
31
+
32
+ /**
33
+ * Read `.secrets.yaml` from a directory, register every value with the
34
+ * global SecretRegistry, and return the parsed map. Returns `null` when
35
+ * the file is absent — callers should treat that as "no secrets to
36
+ * register" rather than a failure.
37
+ */
38
+ export function loadSecretsFile(dir: string): SecretsFile | null {
39
+ const filePath = join(dir, SECRETS_FILENAME);
40
+ if (!existsSync(filePath)) return null;
41
+ const text = readFileSync(filePath, "utf-8");
42
+ let parsed: unknown;
43
+ try {
44
+ parsed = (Bun as any).YAML.parse(text);
45
+ } catch (err) {
46
+ throw new Error(`Failed to parse ${filePath}: ${(err as Error).message}`);
47
+ }
48
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
49
+ throw new Error(`${filePath} must contain a flat YAML object of key: "value" entries`);
50
+ }
51
+
52
+ const values: Record<string, string> = {};
53
+ for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
54
+ if (v == null) continue; // empty placeholder — skip
55
+ if (typeof v === "object") {
56
+ throw new Error(
57
+ `${filePath}: nested values are not supported (key "${k}"). ` +
58
+ `.secrets.yaml is intentionally flat — keep one level of key/value pairs.`,
59
+ );
60
+ }
61
+ values[k] = String(v);
62
+ }
63
+
64
+ const reg = getSecretRegistry();
65
+ reg.registerAll(values);
66
+
67
+ return { filePath, values };
68
+ }
69
+
70
+ /**
71
+ * Walk up a directory chain to find the first `.secrets.yaml` and load
72
+ * it. Used by the env loader so a single secrets file at the API root
73
+ * (`apis/<name>/.secrets.yaml`) is picked up regardless of which
74
+ * subdirectory `zond run` was invoked from.
75
+ */
76
+ export function loadSecretsFromAncestor(start: string, stopAt?: string): SecretsFile | null {
77
+ let dir = start;
78
+ for (let i = 0; i < 8; i++) {
79
+ const file = loadSecretsFile(dir);
80
+ if (file) return file;
81
+ if (stopAt && dir === stopAt) return null;
82
+ const parent = dirname(dir);
83
+ if (parent === dir) return null;
84
+ dir = parent;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Resolve any `@secret:<name>` reference inside an env object against
91
+ * the values from `secrets`. Throws when a referenced name is missing
92
+ * (fail-loud).
93
+ */
94
+ export function resolveSecretRefs(
95
+ envValues: Record<string, string>,
96
+ secrets: SecretsFile | null,
97
+ filePath: string,
98
+ ): Record<string, string> {
99
+ const out: Record<string, string> = { ...envValues };
100
+ for (const [k, v] of Object.entries(out)) {
101
+ const m = typeof v === "string" ? v.match(SECRET_REF_RE) : null;
102
+ if (!m) continue;
103
+ const refName = m[1]!;
104
+ const value = secrets?.values[refName];
105
+ if (value == null) {
106
+ const where = secrets ? secrets.filePath : `${dirname(filePath)}/${SECRETS_FILENAME}`;
107
+ throw new Error(
108
+ `${filePath}: key "${k}" references @secret:${refName} but no such entry exists in ${where}. ` +
109
+ `Add \`${refName}: "<value>"\` to ${where} (or remove the @secret: prefix to use a literal value).`,
110
+ );
111
+ }
112
+ out[k] = value;
113
+ }
114
+ return out;
115
+ }
@@ -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
+