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