@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.
- package/CHANGELOG.md +648 -0
- package/README.md +58 -6
- package/package.json +9 -6
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +178 -7
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -46
- package/src/cli/commands/init/bootstrap.ts +30 -1
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +56 -3
- package/src/cli/commands/init/templates/agents.md +65 -61
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +592 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +842 -53
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +18 -1
- package/src/cli/index.ts +20 -3
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +198 -635
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +5 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +22 -6
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +151 -11
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +42 -16
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +445 -19
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +37 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +103 -13
- package/src/core/generator/suite-generator.ts +419 -111
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +129 -4
- package/src/core/parser/types.ts +19 -1
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +58 -12
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +43 -76
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +183 -149
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +41 -2
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +11 -1
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +58 -1
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +264 -20
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +75 -2
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +89 -17
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +415 -16
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +178 -50
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
- package/src/cli/commands/probe-methods.ts +0 -108
- package/src/cli/commands/probe-validation.ts +0 -124
- package/src/cli/commands/serve.ts +0 -114
- package/src/cli/commands/sync.ts +0 -268
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/diagnostics/render-md.ts +0 -112
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -19
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- 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
|
+
|