@kirrosh/zond 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +22 -21
- package/bin/zond.mjs +23 -0
- package/package.json +13 -16
- package/scripts/npm/postinstall.mjs +76 -0
- package/src/CLAUDE.md +0 -112
- package/src/bun-types.d.ts +0 -5
- package/src/cli/argv.ts +0 -122
- package/src/cli/commands/add-api.ts +0 -146
- package/src/cli/commands/api/annotate/idempotency.ts +0 -59
- package/src/cli/commands/api/annotate/index.ts +0 -880
- package/src/cli/commands/api/annotate/lifecycle.ts +0 -74
- package/src/cli/commands/api/annotate/overlay.ts +0 -206
- package/src/cli/commands/api/annotate/pagination.ts +0 -64
- package/src/cli/commands/api/annotate/prompts.ts +0 -220
- package/src/cli/commands/api/annotate/readback.ts +0 -58
- package/src/cli/commands/api/annotate/resources.ts +0 -91
- package/src/cli/commands/api/annotate/seed-bodies.ts +0 -61
- package/src/cli/commands/audit.ts +0 -786
- package/src/cli/commands/catalog.ts +0 -97
- package/src/cli/commands/check.ts +0 -361
- package/src/cli/commands/checks.ts +0 -1072
- package/src/cli/commands/ci-init.ts +0 -223
- package/src/cli/commands/clean.ts +0 -212
- package/src/cli/commands/cleanup.ts +0 -236
- package/src/cli/commands/completions.ts +0 -192
- package/src/cli/commands/coverage.ts +0 -872
- package/src/cli/commands/db.ts +0 -611
- package/src/cli/commands/describe.ts +0 -120
- package/src/cli/commands/discover.ts +0 -1356
- package/src/cli/commands/doctor.ts +0 -661
- package/src/cli/commands/fixtures.ts +0 -402
- package/src/cli/commands/generate.ts +0 -577
- package/src/cli/commands/init/agents-md.ts +0 -61
- package/src/cli/commands/init/bootstrap.ts +0 -111
- package/src/cli/commands/init/index.ts +0 -244
- package/src/cli/commands/init/skills.ts +0 -141
- package/src/cli/commands/init/templates/agents.md +0 -86
- package/src/cli/commands/init/templates/markdown.d.ts +0 -4
- package/src/cli/commands/init/templates/skills/warm-up-target.md +0 -122
- package/src/cli/commands/init/templates/skills/zond-checks.md +0 -621
- package/src/cli/commands/init/templates/skills/zond-seed.md +0 -114
- package/src/cli/commands/init/templates/skills/zond-triage.md +0 -272
- package/src/cli/commands/init/templates/skills/zond.md +0 -861
- package/src/cli/commands/init/templates/zond-config.yml +0 -14
- package/src/cli/commands/prepare-fixtures.ts +0 -97
- package/src/cli/commands/probe/_seed-bodies.ts +0 -52
- package/src/cli/commands/probe/mass-assignment.ts +0 -594
- package/src/cli/commands/probe/security.ts +0 -537
- package/src/cli/commands/probe/static.ts +0 -255
- package/src/cli/commands/probe/webhooks.ts +0 -163
- package/src/cli/commands/probe.ts +0 -535
- package/src/cli/commands/reference.ts +0 -87
- package/src/cli/commands/refresh-api.ts +0 -227
- package/src/cli/commands/remove-api.ts +0 -150
- package/src/cli/commands/report-bundle.ts +0 -310
- package/src/cli/commands/report.ts +0 -241
- package/src/cli/commands/request.ts +0 -548
- package/src/cli/commands/run.ts +0 -1142
- package/src/cli/commands/schema-from-runs.ts +0 -128
- package/src/cli/commands/secrets.ts +0 -133
- package/src/cli/commands/session.ts +0 -244
- package/src/cli/commands/use.ts +0 -74
- package/src/cli/index.ts +0 -55
- package/src/cli/json-envelope.ts +0 -108
- package/src/cli/json-schemas.ts +0 -314
- package/src/cli/output.ts +0 -40
- package/src/cli/program.ts +0 -219
- package/src/cli/resolve.ts +0 -105
- package/src/cli/runtime.ts +0 -7
- package/src/cli/safe-live.ts +0 -24
- package/src/cli/status-filter.ts +0 -114
- package/src/cli/util/api-context.ts +0 -85
- package/src/cli/version.ts +0 -8
- package/src/core/audit/persist.ts +0 -183
- package/src/core/checks/budget.ts +0 -59
- package/src/core/checks/checks/_crud-helpers.ts +0 -133
- package/src/core/checks/checks/_negative_mutator.ts +0 -133
- package/src/core/checks/checks/_readback-helpers.ts +0 -133
- package/src/core/checks/checks/content_type_conformance.ts +0 -39
- package/src/core/checks/checks/cross_call_references.ts +0 -147
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +0 -219
- package/src/core/checks/checks/ensure_resource_availability.ts +0 -62
- package/src/core/checks/checks/idempotency_replay.ts +0 -242
- package/src/core/checks/checks/ignored_auth.ts +0 -254
- package/src/core/checks/checks/index.ts +0 -68
- package/src/core/checks/checks/lifecycle_transitions.ts +0 -416
- package/src/core/checks/checks/missing_required_header.ts +0 -40
- package/src/core/checks/checks/negative_data_rejection.ts +0 -148
- package/src/core/checks/checks/not_a_server_error.ts +0 -35
- package/src/core/checks/checks/open_cors_on_sensitive.ts +0 -160
- package/src/core/checks/checks/pagination_invariants.ts +0 -419
- package/src/core/checks/checks/positive_data_acceptance.ts +0 -33
- package/src/core/checks/checks/rate_limit_headers_absent.ts +0 -77
- package/src/core/checks/checks/response_headers_conformance.ts +0 -74
- package/src/core/checks/checks/response_schema_conformance.ts +0 -30
- package/src/core/checks/checks/status_code_conformance.ts +0 -132
- package/src/core/checks/checks/unsupported_method.ts +0 -63
- package/src/core/checks/checks/use_after_free.ts +0 -78
- package/src/core/checks/index.ts +0 -30
- package/src/core/checks/mode.ts +0 -82
- package/src/core/checks/recommended-action.ts +0 -68
- package/src/core/checks/registry.ts +0 -78
- package/src/core/checks/runner.ts +0 -1461
- package/src/core/checks/sarif.ts +0 -230
- package/src/core/checks/spec-findings.ts +0 -308
- package/src/core/checks/stateful.ts +0 -121
- package/src/core/checks/types.ts +0 -305
- package/src/core/checks/zond-extensions.ts +0 -73
- package/src/core/classifier/recommended-action.ts +0 -251
- package/src/core/context/current.ts +0 -51
- package/src/core/context/session.ts +0 -78
- package/src/core/coverage/loader.ts +0 -216
- package/src/core/coverage/reasons.ts +0 -300
- package/src/core/diagnostics/db-analysis.ts +0 -666
- package/src/core/diagnostics/failure-class.ts +0 -140
- package/src/core/diagnostics/failure-hints.ts +0 -112
- package/src/core/diagnostics/spec-pointer.ts +0 -99
- package/src/core/diagnostics/suggested-fixes.ts +0 -155
- package/src/core/exporter/case-study/index.ts +0 -270
- package/src/core/exporter/curl.ts +0 -40
- package/src/core/exporter/exporter.ts +0 -48
- package/src/core/exporter/html-report/escape.ts +0 -24
- package/src/core/exporter/html-report/index.ts +0 -479
- package/src/core/exporter/html-report/script.ts +0 -100
- package/src/core/exporter/html-report/styles.ts +0 -408
- package/src/core/generator/catalog-builder.ts +0 -179
- package/src/core/generator/chunker.ts +0 -78
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/coverage-scanner.ts +0 -87
- package/src/core/generator/data-factory.ts +0 -759
- package/src/core/generator/describe.ts +0 -302
- package/src/core/generator/endpoint-warnings.ts +0 -52
- package/src/core/generator/fixtures-builder.ts +0 -332
- package/src/core/generator/index.ts +0 -14
- package/src/core/generator/openapi-reader.ts +0 -297
- package/src/core/generator/path-param-disambig.ts +0 -140
- package/src/core/generator/resources-builder.ts +0 -898
- package/src/core/generator/schema-utils.ts +0 -112
- package/src/core/generator/serializer.ts +0 -343
- package/src/core/generator/suite-generator.ts +0 -1301
- package/src/core/generator/types.ts +0 -63
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/lint/affects.ts +0 -28
- package/src/core/lint/config.ts +0 -96
- package/src/core/lint/format.ts +0 -42
- package/src/core/lint/index.ts +0 -94
- package/src/core/lint/reporter.ts +0 -128
- package/src/core/lint/rules/consistency.ts +0 -158
- package/src/core/lint/rules/heuristics.ts +0 -97
- package/src/core/lint/rules/strictness.ts +0 -109
- package/src/core/lint/types.ts +0 -96
- package/src/core/lint/walker.ts +0 -248
- package/src/core/meta/meta-store.ts +0 -11
- package/src/core/output/README.md +0 -73
- package/src/core/output/index.ts +0 -13
- package/src/core/output/run.ts +0 -91
- package/src/core/output/types.ts +0 -122
- package/src/core/parser/dynamic-values.ts +0 -160
- package/src/core/parser/env-interpolation.ts +0 -104
- package/src/core/parser/filter.ts +0 -97
- package/src/core/parser/schema.ts +0 -359
- package/src/core/parser/types.ts +0 -117
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +0 -181
- package/src/core/probe/bootstrap.ts +0 -34
- package/src/core/probe/dry-run-envelope.ts +0 -61
- package/src/core/probe/mass-assignment/classify.ts +0 -175
- package/src/core/probe/mass-assignment/cleanup.ts +0 -52
- package/src/core/probe/mass-assignment/digest.ts +0 -114
- package/src/core/probe/mass-assignment/orchestrator.ts +0 -459
- package/src/core/probe/mass-assignment/regression.ts +0 -141
- package/src/core/probe/mass-assignment/suspects.ts +0 -92
- package/src/core/probe/mass-assignment/types.ts +0 -135
- package/src/core/probe/mass-assignment-probe-class.ts +0 -198
- package/src/core/probe/mass-assignment-probe.ts +0 -27
- package/src/core/probe/mass-assignment-template.ts +0 -240
- package/src/core/probe/method-probe.ts +0 -164
- package/src/core/probe/method-shared.ts +0 -69
- package/src/core/probe/negative-probe.ts +0 -691
- package/src/core/probe/orphan-tracker.ts +0 -188
- package/src/core/probe/path-discovery.ts +0 -439
- package/src/core/probe/probe-harness.ts +0 -119
- package/src/core/probe/registry.ts +0 -89
- package/src/core/probe/runner.ts +0 -136
- package/src/core/probe/security/baseline.ts +0 -174
- package/src/core/probe/security/classify.ts +0 -341
- package/src/core/probe/security/cleanup.ts +0 -125
- package/src/core/probe/security/detectors.ts +0 -71
- package/src/core/probe/security/digest.ts +0 -104
- package/src/core/probe/security/orchestrator.ts +0 -398
- package/src/core/probe/security/regression.ts +0 -103
- package/src/core/probe/security/types.ts +0 -151
- package/src/core/probe/security-probe-class.ts +0 -207
- package/src/core/probe/security-probe.ts +0 -32
- package/src/core/probe/shared.ts +0 -531
- package/src/core/probe/static-probe-class.ts +0 -125
- package/src/core/probe/types.ts +0 -165
- package/src/core/probe/verdict-aggregator.ts +0 -33
- package/src/core/probe/webhooks-probe.ts +0 -282
- package/src/core/reporter/console.ts +0 -240
- package/src/core/reporter/index.ts +0 -22
- package/src/core/reporter/json.ts +0 -22
- package/src/core/reporter/junit.ts +0 -93
- package/src/core/reporter/ndjson.ts +0 -37
- package/src/core/reporter/types.ts +0 -15
- package/src/core/runner/assertions.ts +0 -383
- package/src/core/runner/async-pool.ts +0 -108
- package/src/core/runner/auth-path.ts +0 -8
- package/src/core/runner/ci-context.ts +0 -72
- package/src/core/runner/executor.ts +0 -652
- package/src/core/runner/expr-eval.ts +0 -41
- package/src/core/runner/form-encode.ts +0 -41
- package/src/core/runner/http-client.ts +0 -224
- package/src/core/runner/learn-drift.ts +0 -293
- package/src/core/runner/preflight-vars.ts +0 -153
- package/src/core/runner/progress-tracker.ts +0 -73
- package/src/core/runner/rate-limiter.ts +0 -185
- package/src/core/runner/run-kind.ts +0 -45
- package/src/core/runner/schema-validator.ts +0 -308
- package/src/core/runner/send-request.ts +0 -232
- package/src/core/runner/transforms.ts +0 -65
- package/src/core/runner/types.ts +0 -94
- package/src/core/secrets/registry.ts +0 -164
- package/src/core/secrets/secrets-file.ts +0 -115
- package/src/core/selectors/operation-filter.ts +0 -144
- package/src/core/setup-api.ts +0 -590
- package/src/core/severity/category.ts +0 -94
- package/src/core/severity/index.ts +0 -58
- package/src/core/spec/infer-schema.ts +0 -102
- package/src/core/spec/layers.ts +0 -154
- package/src/core/spec/merge-specs.ts +0 -156
- package/src/core/spec/schema-from-runs.ts +0 -117
- package/src/core/spec/schema-overlay.ts +0 -130
- package/src/core/util/ajv.ts +0 -13
- package/src/core/util/format-eta.ts +0 -21
- package/src/core/util/headers.ts +0 -9
- package/src/core/util/url.ts +0 -24
- package/src/core/utils.ts +0 -13
- package/src/core/workspace/config.ts +0 -129
- package/src/core/workspace/fixture-gap-report.ts +0 -84
- package/src/core/workspace/fixture-gaps.ts +0 -71
- package/src/core/workspace/manifest.ts +0 -283
- package/src/core/workspace/output-rotation.ts +0 -62
- package/src/core/workspace/root.ts +0 -96
- package/src/core/workspace/triage-path.ts +0 -87
- package/src/db/lint-runs.ts +0 -47
- package/src/db/migrate.ts +0 -128
- package/src/db/migrations/0001_run_kind.sql +0 -25
- package/src/db/migrations/0002_run_kind_request.sql +0 -59
- package/src/db/migrations/sql.d.ts +0 -4
- package/src/db/queries/collections.ts +0 -133
- package/src/db/queries/coverage.ts +0 -9
- package/src/db/queries/dashboard.ts +0 -59
- package/src/db/queries/results.ts +0 -216
- package/src/db/queries/runs.ts +0 -289
- package/src/db/queries/sessions.ts +0 -42
- package/src/db/queries/settings.ts +0 -28
- package/src/db/queries/types.ts +0 -172
- package/src/db/queries.ts +0 -75
- package/src/db/schema.ts +0 -298
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "./openapi-reader.ts";
|
|
2
|
-
export { serializeSuite } from "./serializer.ts";
|
|
3
|
-
export type { RawSuite, RawStep } from "./serializer.ts";
|
|
4
|
-
export { scanCoveredEndpoints, filterUncoveredEndpoints, normalizePath, specPathToRegex } from "./coverage-scanner.ts";
|
|
5
|
-
export type { CoveredEndpoint } from "./coverage-scanner.ts";
|
|
6
|
-
export { analyzeEndpoints } from "./endpoint-warnings.ts";
|
|
7
|
-
export type { EndpointWarning, WarningCode } from "./endpoint-warnings.ts";
|
|
8
|
-
export type { EndpointInfo, ResponseInfo, GenerateOptions, SecuritySchemeInfo, CrudGroup } from "./types.ts";
|
|
9
|
-
export { buildCatalog, serializeCatalog } from "./catalog-builder.ts";
|
|
10
|
-
export type { ApiCatalog, CatalogEndpoint } from "./catalog-builder.ts";
|
|
11
|
-
export { buildApiResourceMap, serializeApiResourceMap } from "./resources-builder.ts";
|
|
12
|
-
export type { ApiResourceMap, ApiResourceEntry, ResourceFkRef } from "./resources-builder.ts";
|
|
13
|
-
export { buildApiFixtureManifest, serializeApiFixtureManifest } from "./fixtures-builder.ts";
|
|
14
|
-
export type { ApiFixtureManifest, FixtureRequirement, FixtureSource } from "./fixtures-builder.ts";
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { rootCertificates } from "node:tls";
|
|
3
|
-
import { dereference } from "@readme/openapi-parser";
|
|
4
|
-
import type { OpenAPIV3 } from "openapi-types";
|
|
5
|
-
import type { EndpointInfo, ResponseInfo, SecuritySchemeInfo } from "./types.ts";
|
|
6
|
-
import { disambiguateGenericPathParams } from "./path-param-disambig.ts";
|
|
7
|
-
|
|
8
|
-
const HTTP_METHODS = ["get", "post", "put", "patch", "delete"] as const;
|
|
9
|
-
|
|
10
|
-
export interface SpecFetchTlsOptions {
|
|
11
|
-
/** Disable TLS verification entirely (bun `--insecure`). Last resort. */
|
|
12
|
-
insecure?: boolean;
|
|
13
|
-
/** Path to a PEM CA bundle to trust in addition to the public roots.
|
|
14
|
-
* Falls back to the `NODE_EXTRA_CA_CERTS` env var. */
|
|
15
|
-
caPath?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** MF1 (ARV-367): resolve the bun `fetch` `tls` option for a spec fetch so a
|
|
19
|
-
* self-signed / internal corporate CA validates *without* disabling TLS.
|
|
20
|
-
*
|
|
21
|
-
* Precedence: `insecure` (verification off) > explicit `caPath` /
|
|
22
|
-
* `NODE_EXTRA_CA_CERTS` (extra CA APPENDED to the public roots — never
|
|
23
|
-
* replacing them, so public specs keep validating) > default (undefined).
|
|
24
|
-
* Returns undefined when nothing special is needed. Throws if a CA path is
|
|
25
|
-
* set but unreadable — a misconfigured CA should surface, not fall through
|
|
26
|
-
* to a confusing "self signed certificate" error. */
|
|
27
|
-
export function resolveSpecFetchTls(
|
|
28
|
-
options?: SpecFetchTlsOptions,
|
|
29
|
-
): { rejectUnauthorized: false } | { ca: string[] } | undefined {
|
|
30
|
-
if (options?.insecure) return { rejectUnauthorized: false };
|
|
31
|
-
const caPath = options?.caPath ?? process.env.NODE_EXTRA_CA_CERTS;
|
|
32
|
-
if (caPath) {
|
|
33
|
-
let extra: string;
|
|
34
|
-
try {
|
|
35
|
-
extra = readFileSync(caPath, "utf8");
|
|
36
|
-
} catch (e) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
`CA bundle not readable: ${caPath} (${(e as Error).message}). ` +
|
|
39
|
-
`Set --ca / NODE_EXTRA_CA_CERTS to a valid PEM file, or use --insecure.`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (extra.trim()) return { ca: [extra, ...rootCertificates] };
|
|
43
|
-
}
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function readOpenApiSpec(specPath: string, options?: SpecFetchTlsOptions): Promise<OpenAPIV3.Document> {
|
|
48
|
-
// For HTTP URLs, fetch the spec first then dereference the parsed object
|
|
49
|
-
if (specPath.startsWith("http://") || specPath.startsWith("https://")) {
|
|
50
|
-
const tls = resolveSpecFetchTls(options);
|
|
51
|
-
const resp = await fetch(specPath, { ...(tls ? { tls } : {}) });
|
|
52
|
-
if (!resp.ok) throw new Error(`Failed to fetch spec: ${resp.status} ${resp.statusText}`);
|
|
53
|
-
const spec = await resp.json();
|
|
54
|
-
const api = await dereference(spec as string);
|
|
55
|
-
return api as OpenAPIV3.Document;
|
|
56
|
-
}
|
|
57
|
-
const api = await dereference(specPath);
|
|
58
|
-
return api as OpenAPIV3.Document;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** ARV-376: align declared path-param names with the path template when they
|
|
62
|
-
* diverge (spec quirk: path `/byid/{id}` but param declared `byid_id`). The
|
|
63
|
-
* template is authoritative for substitution, so unmatched params are renamed
|
|
64
|
-
* to the unmatched template segment names, in positional order. No-op when
|
|
65
|
-
* names already agree (the common, well-formed case). Mutates in place. */
|
|
66
|
-
export function reconcilePathParamNames(
|
|
67
|
-
path: string,
|
|
68
|
-
parameters: OpenAPIV3.ParameterObject[],
|
|
69
|
-
): void {
|
|
70
|
-
const tplNames = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]!);
|
|
71
|
-
if (tplNames.length === 0) return;
|
|
72
|
-
const pathParams = parameters.filter((p) => p.in === "path");
|
|
73
|
-
const declared = new Set(pathParams.map((p) => p.name));
|
|
74
|
-
const unmatchedTpl = tplNames.filter((n) => !declared.has(n));
|
|
75
|
-
const unmatchedParams = pathParams.filter((p) => !tplNames.includes(p.name));
|
|
76
|
-
// Only reconcile a clean 1:1 correspondence — if the counts differ we can't
|
|
77
|
-
// safely guess the mapping, so leave the (already odd) spec untouched.
|
|
78
|
-
if (unmatchedTpl.length === 0 || unmatchedTpl.length !== unmatchedParams.length) return;
|
|
79
|
-
unmatchedParams.forEach((p, i) => {
|
|
80
|
-
(p as { name: string }).name = unmatchedTpl[i]!;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function extractSecuritySchemes(doc: OpenAPIV3.Document): SecuritySchemeInfo[] {
|
|
85
|
-
const schemes: SecuritySchemeInfo[] = [];
|
|
86
|
-
const securitySchemes = doc.components?.securitySchemes;
|
|
87
|
-
if (!securitySchemes) return schemes;
|
|
88
|
-
|
|
89
|
-
for (const [name, schemeObj] of Object.entries(securitySchemes)) {
|
|
90
|
-
const scheme = schemeObj as OpenAPIV3.SecuritySchemeObject;
|
|
91
|
-
const info: SecuritySchemeInfo = {
|
|
92
|
-
name,
|
|
93
|
-
type: scheme.type as SecuritySchemeInfo["type"],
|
|
94
|
-
};
|
|
95
|
-
if (scheme.type === "http") {
|
|
96
|
-
info.scheme = scheme.scheme;
|
|
97
|
-
info.bearerFormat = scheme.bearerFormat;
|
|
98
|
-
}
|
|
99
|
-
if (scheme.type === "apiKey") {
|
|
100
|
-
info.in = scheme.in;
|
|
101
|
-
info.apiKeyName = scheme.name;
|
|
102
|
-
}
|
|
103
|
-
schemes.push(info);
|
|
104
|
-
}
|
|
105
|
-
return schemes;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
109
|
-
const endpoints: EndpointInfo[] = [];
|
|
110
|
-
|
|
111
|
-
if (!doc.paths) return endpoints;
|
|
112
|
-
|
|
113
|
-
for (const [path, pathItem] of Object.entries(doc.paths)) {
|
|
114
|
-
if (!pathItem) continue;
|
|
115
|
-
|
|
116
|
-
for (const method of HTTP_METHODS) {
|
|
117
|
-
const operation = pathItem[method] as OpenAPIV3.OperationObject | undefined;
|
|
118
|
-
if (!operation) continue;
|
|
119
|
-
|
|
120
|
-
const parameters: OpenAPIV3.ParameterObject[] = [];
|
|
121
|
-
|
|
122
|
-
// Skip circular-ref sentinel stubs emitted by decycleSchema —
|
|
123
|
-
// they look like `{ "x-circular": true }` (no .name, no .in) and
|
|
124
|
-
// crash downstream code that expects p.name/p.in. ARV-200 (R10/F1).
|
|
125
|
-
const isUsableParam = (p: any): p is OpenAPIV3.ParameterObject =>
|
|
126
|
-
p != null && typeof p === "object" && typeof p.name === "string" && typeof p.in === "string";
|
|
127
|
-
|
|
128
|
-
// Path-level parameters
|
|
129
|
-
if (pathItem.parameters) {
|
|
130
|
-
for (const p of pathItem.parameters) {
|
|
131
|
-
if (!isUsableParam(p)) continue;
|
|
132
|
-
parameters.push(p as OpenAPIV3.ParameterObject);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Operation-level parameters (override path-level)
|
|
137
|
-
if (operation.parameters) {
|
|
138
|
-
for (const p of operation.parameters) {
|
|
139
|
-
if (!isUsableParam(p)) continue;
|
|
140
|
-
const param = p as OpenAPIV3.ParameterObject;
|
|
141
|
-
const existingIdx = parameters.findIndex(
|
|
142
|
-
(existing) => existing.name === param.name && existing.in === param.in,
|
|
143
|
-
);
|
|
144
|
-
if (existingIdx >= 0) {
|
|
145
|
-
parameters[existingIdx] = param;
|
|
146
|
-
} else {
|
|
147
|
-
parameters.push(param);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ARV-376: reconcile a malformed spec where a path *template* segment
|
|
153
|
-
// (`/byid/{id}`) disagrees with the declared path *parameter* name
|
|
154
|
-
// (`byid_id`) — a docgen-style quirk. The path template is what the
|
|
155
|
-
// runner substitutes, so it wins: rename the diverging param(s) to the
|
|
156
|
-
// template segment name(s), positionally. Without this the fixture var
|
|
157
|
-
// (from the param) never matches the `{...}` in the path, and every
|
|
158
|
-
// downstream layer (disambig, manifest, resource-graph) diverges.
|
|
159
|
-
reconcilePathParamNames(path, parameters);
|
|
160
|
-
|
|
161
|
-
// Request body schema + content type
|
|
162
|
-
let requestBodySchema: OpenAPIV3.SchemaObject | undefined;
|
|
163
|
-
let requestBodyContentType: string | undefined;
|
|
164
|
-
if (operation.requestBody) {
|
|
165
|
-
const rb = operation.requestBody as OpenAPIV3.RequestBodyObject;
|
|
166
|
-
if (rb.content) {
|
|
167
|
-
// Prefer application/json, fall back to first available
|
|
168
|
-
const contentTypes = Object.keys(rb.content);
|
|
169
|
-
requestBodyContentType = contentTypes.includes("application/json")
|
|
170
|
-
? "application/json"
|
|
171
|
-
: contentTypes[0];
|
|
172
|
-
const chosen = rb.content[requestBodyContentType!];
|
|
173
|
-
if (chosen?.schema) {
|
|
174
|
-
requestBodySchema = chosen.schema as OpenAPIV3.SchemaObject;
|
|
175
|
-
// OpenAPI allows examples at the media-type level (sibling to schema).
|
|
176
|
-
// Lift them onto the schema so the generator sees a single signal.
|
|
177
|
-
if (requestBodySchema.example === undefined) {
|
|
178
|
-
if ((chosen as OpenAPIV3.MediaTypeObject).example !== undefined) {
|
|
179
|
-
requestBodySchema = {
|
|
180
|
-
...requestBodySchema,
|
|
181
|
-
example: (chosen as OpenAPIV3.MediaTypeObject).example,
|
|
182
|
-
};
|
|
183
|
-
} else if (chosen.examples) {
|
|
184
|
-
const firstNamed = Object.values(chosen.examples)[0];
|
|
185
|
-
if (firstNamed && typeof firstNamed === "object" && "value" in firstNamed) {
|
|
186
|
-
requestBodySchema = {
|
|
187
|
-
...requestBodySchema,
|
|
188
|
-
example: (firstNamed as OpenAPIV3.ExampleObject).value,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Responses
|
|
198
|
-
const responses: ResponseInfo[] = [];
|
|
199
|
-
const responseContentTypesSet = new Set<string>();
|
|
200
|
-
if (operation.responses) {
|
|
201
|
-
for (const [statusCode, responseObj] of Object.entries(operation.responses)) {
|
|
202
|
-
const parsedStatus = parseInt(statusCode, 10);
|
|
203
|
-
// Skip non-numeric keys like "default" — they have no asserting status code.
|
|
204
|
-
if (!Number.isFinite(parsedStatus)) continue;
|
|
205
|
-
const resp = responseObj as OpenAPIV3.ResponseObject;
|
|
206
|
-
const info: ResponseInfo = {
|
|
207
|
-
statusCode: parsedStatus,
|
|
208
|
-
description: resp.description || "",
|
|
209
|
-
};
|
|
210
|
-
if (resp.content) {
|
|
211
|
-
for (const ct of Object.keys(resp.content)) {
|
|
212
|
-
responseContentTypesSet.add(ct);
|
|
213
|
-
}
|
|
214
|
-
const jsonContent = resp.content["application/json"];
|
|
215
|
-
if (jsonContent?.schema) {
|
|
216
|
-
info.schema = jsonContent.schema as OpenAPIV3.SchemaObject;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
responses.push(info);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Security: operation-level overrides doc-level
|
|
224
|
-
const securityReqs = operation.security ?? doc.security ?? [];
|
|
225
|
-
const security = securityReqs.flatMap((req) => Object.keys(req));
|
|
226
|
-
|
|
227
|
-
// ETag optimistic locking: detect if endpoint requires If-Match header
|
|
228
|
-
const requiresEtag =
|
|
229
|
-
responses.some(r => r.statusCode === 412) ||
|
|
230
|
-
parameters.some(p => p.name.toLowerCase() === "if-match" && p.in === "header");
|
|
231
|
-
|
|
232
|
-
// ARV-189: harvest `x-*` vendor extensions. Operation-level wins on
|
|
233
|
-
// key collision over path-level so spec authors can default at the
|
|
234
|
-
// path and override per-operation. Empty result = undefined (avoids
|
|
235
|
-
// a churn-y `extensions: {}` field in serialised endpoint snapshots).
|
|
236
|
-
const extensions = collectExtensions(pathItem, operation);
|
|
237
|
-
|
|
238
|
-
endpoints.push({
|
|
239
|
-
path,
|
|
240
|
-
method: method.toUpperCase(),
|
|
241
|
-
operationId: operation.operationId,
|
|
242
|
-
summary: operation.summary,
|
|
243
|
-
tags: operation.tags ?? [],
|
|
244
|
-
parameters,
|
|
245
|
-
requestBodySchema,
|
|
246
|
-
requestBodyContentType,
|
|
247
|
-
responseContentTypes: [...responseContentTypesSet],
|
|
248
|
-
responses,
|
|
249
|
-
security,
|
|
250
|
-
deprecated: (operation.deprecated ?? false) || isMarkedDeprecatedInText(operation.summary, operation.description, operation.operationId),
|
|
251
|
-
requiresEtag,
|
|
252
|
-
...(extensions ? { extensions } : {}),
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ARV-40: when generic path-param names (`{id}`, `{slug}`, ...) collide
|
|
258
|
-
// across multiple resources, rewrite each to `<parent_singular>_<param>`
|
|
259
|
-
// so the manifest derives per-resource vars and tests stop sharing one
|
|
260
|
-
// global `id`. In-memory only; on-disk spec stays untouched.
|
|
261
|
-
return disambiguateGenericPathParams(endpoints);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/** Collect `x-*` vendor extensions from a path item and operation,
|
|
265
|
-
* with operation values winning on key collision. Returns undefined
|
|
266
|
-
* when neither has any so callers can omit the field cleanly. */
|
|
267
|
-
function collectExtensions(
|
|
268
|
-
pathItem: OpenAPIV3.PathItemObject,
|
|
269
|
-
operation: OpenAPIV3.OperationObject,
|
|
270
|
-
): Record<string, unknown> | undefined {
|
|
271
|
-
const out: Record<string, unknown> = {};
|
|
272
|
-
for (const [k, v] of Object.entries(pathItem)) {
|
|
273
|
-
if (k.startsWith("x-")) out[k] = v;
|
|
274
|
-
}
|
|
275
|
-
for (const [k, v] of Object.entries(operation)) {
|
|
276
|
-
if (k.startsWith("x-")) out[k] = v;
|
|
277
|
-
}
|
|
278
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/** Spec authors often mark endpoints as deprecated in the summary or
|
|
282
|
-
* description text instead of (or in addition to) the `deprecated: true`
|
|
283
|
-
* flag — common across many SaaS and legacy specs. Without this
|
|
284
|
-
* fallback, generator emits CRUD suites whose POST returns 404 from a dead
|
|
285
|
-
* endpoint. (TASK-245) */
|
|
286
|
-
/** Matches `(DEPRECATED) ...`, `[DEPRECATED] ...`, `DEPRECATED: ...` at the
|
|
287
|
-
* start of a string. Also matches markdown `## Deprecated` headings, which
|
|
288
|
-
* some spec authors use in operation `description` to flag end-of-life
|
|
289
|
-
* endpoints. */
|
|
290
|
-
const DEPRECATED_PREFIX_RE = /^\s*[\(\[]?\s*DEPRECATED\s*[\)\]:\-—\s]/i;
|
|
291
|
-
const DEPRECATED_HEADING_RE = /^\s*#+\s*Deprecated\b/im;
|
|
292
|
-
function isMarkedDeprecatedInText(summary?: string, description?: string, operationId?: string): boolean {
|
|
293
|
-
if (summary && DEPRECATED_PREFIX_RE.test(summary)) return true;
|
|
294
|
-
if (operationId && DEPRECATED_PREFIX_RE.test(operationId)) return true;
|
|
295
|
-
if (description && (DEPRECATED_PREFIX_RE.test(description) || DEPRECATED_HEADING_RE.test(description))) return true;
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Disambiguate generic path-parameter names that collide across resources.
|
|
3
|
-
*
|
|
4
|
-
* Some OpenAPI specs declare item paths as `/<resource>/{id}` instead of
|
|
5
|
-
* `/<resource>/{<resource>_id}` — fine
|
|
6
|
-
* within one resource, catastrophic across many: the manifest derives one
|
|
7
|
-
* global `id` var, `.env.yaml` stores one value, and N>1 CRUD suites end up
|
|
8
|
-
* pointing at the same uuid (false 404 at best; false 200 against a stranger's
|
|
9
|
-
* object at worst — the GET returns a real object so the test "passes" while
|
|
10
|
-
* having tested nothing of the target resource).
|
|
11
|
-
*
|
|
12
|
-
* ARV-40 fix: when a generic param name (`id`, `slug`, `uuid`, `key`,
|
|
13
|
-
* `identifier`) appears under more than one distinct parent collection in the
|
|
14
|
-
* spec, rewrite each occurrence to `<parent_singular>_<param>` in
|
|
15
|
-
* EndpointInfo.path AND the matching parameter entry. All downstream code
|
|
16
|
-
* (resource map, fixture manifest, suite generator, mass-assignment probe)
|
|
17
|
-
* then sees per-resource var names without any extra plumbing.
|
|
18
|
-
*
|
|
19
|
-
* The on-disk OpenAPI spec.json is untouched — this is an in-memory
|
|
20
|
-
* normalisation. Coverage matches by structural shape (`{x}` is `{x}`), and
|
|
21
|
-
* the runner substitutes `{{var}}` after path templating, so renaming
|
|
22
|
-
* `{id}` → `{template_id}` only affects how zond *names* the variable.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import type { EndpointInfo } from "./types.ts";
|
|
26
|
-
|
|
27
|
-
const GENERIC_PARAM_NAMES = new Set(["id", "slug", "uuid", "key", "name", "identifier", "code"]);
|
|
28
|
-
|
|
29
|
-
// ARV-376: read-by-id accessor markers. `/business-segment20/byid/{id}` names
|
|
30
|
-
// the resource `business-segment20`, not `byid` — the `byid` (or `by-id`)
|
|
31
|
-
// segment is a "get by id" verb, not the owning collection. Without skipping
|
|
32
|
-
// it, every `/*/byid/{id}` across the spec collapses to one global `byid_id`
|
|
33
|
-
// var with no owner resource, so prepare-fixtures reports `miss-no-list`.
|
|
34
|
-
const ACCESSOR_MARKER_SEGS = new Set(["byid", "by-id", "by_id"]);
|
|
35
|
-
|
|
36
|
-
// ARV-381: version segments (`v30`, `v2`) sit between the collection and its
|
|
37
|
-
// id/code param (`/api/macros/v30/{code}`). owningCollectionForPathParam +
|
|
38
|
-
// CRUD resource-name derivation ALREADY strip these via
|
|
39
|
-
// stripTrailingVersionSegments; the disambig parent-walk must skip them too,
|
|
40
|
-
// or `macros_v30_code` (scoped past the version) has no matching resource in
|
|
41
|
-
// the graph → miss-no-list. Same regex as stripTrailingVersionSegments.
|
|
42
|
-
function isVersionSeg(seg: string): boolean {
|
|
43
|
-
return /^v(ersion)?\d+$/i.test(seg);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isParamSeg(seg: string | undefined): boolean {
|
|
47
|
-
return !!seg && /^\{[^}]+\}$/.test(seg);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Segments the parent-walk steps over to reach the owning collection:
|
|
51
|
-
* read-by-id accessor markers and version segments. */
|
|
52
|
-
function isSkippableSeg(seg: string): boolean {
|
|
53
|
-
return ACCESSOR_MARKER_SEGS.has(seg.toLowerCase()) || isVersionSeg(seg);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** English singularization sufficient for resource-collection nouns. */
|
|
57
|
-
function singularize(word: string): string {
|
|
58
|
-
if (word.length > 3 && /ies$/i.test(word)) return word.slice(0, -3) + "y";
|
|
59
|
-
if (word.length > 3 && /(ch|sh|x|ss|z)es$/i.test(word)) return word.slice(0, -2);
|
|
60
|
-
if (word.length > 1 && /[^s]s$/i.test(word)) return word.slice(0, -1);
|
|
61
|
-
return word;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Turn a path segment (`contact-properties`) into an identifier stem (`contact_property`). */
|
|
65
|
-
function segToStem(seg: string): string {
|
|
66
|
-
return singularize(seg).replace(/[^a-zA-Z0-9]+/g, "_").toLowerCase();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface Occurrence {
|
|
70
|
-
ep: EndpointInfo;
|
|
71
|
-
/** Index of the {param} segment in the path. */
|
|
72
|
-
segIdx: number;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Mutates endpoints in place; returns the same array for chaining. */
|
|
76
|
-
export function disambiguateGenericPathParams(endpoints: EndpointInfo[]): EndpointInfo[] {
|
|
77
|
-
// param-name → parent-seg → occurrences
|
|
78
|
-
const byParamParent = new Map<string, Map<string, Occurrence[]>>();
|
|
79
|
-
|
|
80
|
-
for (const ep of endpoints) {
|
|
81
|
-
const segs = ep.path.split("/");
|
|
82
|
-
for (let i = 0; i < segs.length; i++) {
|
|
83
|
-
const m = /^\{([^}]+)\}$/.exec(segs[i]!);
|
|
84
|
-
if (!m) continue;
|
|
85
|
-
const paramName = m[1]!;
|
|
86
|
-
if (!GENERIC_PARAM_NAMES.has(paramName.toLowerCase())) continue;
|
|
87
|
-
// Walk back to nearest non-param non-empty segment as "parent",
|
|
88
|
-
// skipping read-by-id accessor markers (`byid`, ARV-376) and version
|
|
89
|
-
// segments (`v30`, ARV-381) so the owning collection — not an accessor
|
|
90
|
-
// verb or version marker — names the param.
|
|
91
|
-
let parent: string | undefined;
|
|
92
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
93
|
-
const s = segs[j]!;
|
|
94
|
-
if (s && !isParamSeg(s) && !isSkippableSeg(s)) {
|
|
95
|
-
parent = s;
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (!parent) continue;
|
|
100
|
-
let perParent = byParamParent.get(paramName);
|
|
101
|
-
if (!perParent) {
|
|
102
|
-
perParent = new Map();
|
|
103
|
-
byParamParent.set(paramName, perParent);
|
|
104
|
-
}
|
|
105
|
-
const arr = perParent.get(parent) ?? [];
|
|
106
|
-
arr.push({ ep, segIdx: i });
|
|
107
|
-
perParent.set(parent, arr);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
for (const [paramName, perParent] of byParamParent) {
|
|
112
|
-
// Only rename when the param collides across ≥2 parents — single-resource
|
|
113
|
-
// use of `{id}` stays as-is to avoid churning user .env.yaml entries on
|
|
114
|
-
// APIs where the convention isn't a problem.
|
|
115
|
-
if (perParent.size < 2) continue;
|
|
116
|
-
const lowerParam = paramName.toLowerCase();
|
|
117
|
-
for (const [parent, occs] of perParent) {
|
|
118
|
-
const stem = segToStem(parent);
|
|
119
|
-
if (!stem) continue;
|
|
120
|
-
const newName = lowerParam === "id" ? `${stem}_id` : `${stem}_${lowerParam}`;
|
|
121
|
-
// Skip rename if newName collides with an unrelated existing name
|
|
122
|
-
// for the same endpoint (would corrupt parameters[]). Cheap guard.
|
|
123
|
-
for (const occ of occs) {
|
|
124
|
-
// ARV-183: preserve the original spec path before mutating, so
|
|
125
|
-
// downstream checks (status_code_conformance, response_headers_conformance)
|
|
126
|
-
// can still look up `doc.paths[...]` by string equality. Set only
|
|
127
|
-
// on first rename — subsequent renames of the same endpoint keep
|
|
128
|
-
// the truly original path.
|
|
129
|
-
if (occ.ep.originalPath === undefined) occ.ep.originalPath = occ.ep.path;
|
|
130
|
-
const segs = occ.ep.path.split("/");
|
|
131
|
-
segs[occ.segIdx] = `{${newName}}`;
|
|
132
|
-
occ.ep.path = segs.join("/");
|
|
133
|
-
const param = occ.ep.parameters.find(p => p.name === paramName && p.in === "path");
|
|
134
|
-
if (param) (param as { name: string }).name = newName;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return endpoints;
|
|
140
|
-
}
|