@kirrosh/zond 0.23.0 → 0.26.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 +164 -1
- package/README.md +8 -7
- package/package.json +2 -3
- package/src/CLAUDE.md +112 -0
- package/src/cli/commands/add-api.ts +19 -7
- package/src/cli/commands/api/annotate/index.ts +359 -4
- package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
- package/src/cli/commands/api/annotate/overlay.ts +1 -1
- package/src/cli/commands/api/annotate/pagination.ts +10 -6
- package/src/cli/commands/api/annotate/prompts.ts +39 -2
- package/src/cli/commands/audit.ts +360 -54
- package/src/cli/commands/check.ts +15 -2
- package/src/cli/commands/checks.ts +352 -36
- package/src/cli/commands/cleanup.ts +4 -30
- package/src/cli/commands/coverage.ts +275 -57
- package/src/cli/commands/db.ts +311 -8
- package/src/cli/commands/discover.ts +281 -161
- package/src/cli/commands/doctor.ts +57 -3
- package/src/cli/commands/fixtures.ts +1 -1
- package/src/cli/commands/generate.ts +24 -7
- package/src/cli/commands/init/bootstrap.ts +4 -1
- package/src/cli/commands/init/index.ts +2 -2
- package/src/cli/commands/init/skills.ts +43 -0
- package/src/cli/commands/init/templates/agents.md +12 -3
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
- package/src/cli/commands/init/templates/skills/zond.md +274 -64
- package/src/cli/commands/init/templates/zond-config.yml +1 -1
- package/src/cli/commands/prepare-fixtures.ts +14 -52
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +101 -10
- package/src/cli/commands/probe/security.ts +95 -12
- package/src/cli/commands/probe/webhooks.ts +2 -0
- package/src/cli/commands/probe.ts +87 -11
- package/src/cli/commands/refresh-api.ts +59 -1
- package/src/cli/commands/report-bundle.ts +3 -11
- package/src/cli/commands/request.ts +116 -0
- package/src/cli/commands/run.ts +33 -5
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/json-envelope.ts +0 -20
- package/src/cli/json-schemas.ts +51 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +5 -4
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +0 -10
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -0
- package/src/core/checks/checks/cross_call_references.ts +17 -4
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/idempotency_replay.ts +1 -5
- package/src/core/checks/checks/ignored_auth.ts +44 -1
- package/src/core/checks/checks/index.ts +3 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
- package/src/core/checks/checks/negative_data_rejection.ts +119 -16
- package/src/core/checks/checks/not_a_server_error.ts +8 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
- package/src/core/checks/checks/pagination_invariants.ts +298 -117
- package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
- package/src/core/checks/checks/status_code_conformance.ts +78 -7
- package/src/core/checks/mode.ts +3 -0
- package/src/core/checks/recommended-action.ts +5 -1
- package/src/core/checks/runner.ts +614 -27
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/types.ts +117 -1
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +35 -6
- package/src/core/coverage/loader.ts +31 -0
- package/src/core/diagnostics/db-analysis.ts +200 -106
- package/src/core/diagnostics/failure-class.ts +21 -1
- package/src/core/diagnostics/failure-hints.ts +4 -208
- package/src/core/diagnostics/suggested-fixes.ts +2 -3
- package/src/core/generator/chunker.ts +1 -8
- package/src/core/generator/data-factory.ts +199 -61
- package/src/core/generator/fixtures-builder.ts +38 -31
- package/src/core/generator/index.ts +0 -2
- package/src/core/generator/openapi-reader.ts +98 -4
- package/src/core/generator/path-param-disambig.ts +30 -4
- package/src/core/generator/resources-builder.ts +276 -26
- package/src/core/generator/schema-utils.ts +22 -0
- package/src/core/generator/suite-generator.ts +168 -15
- package/src/core/generator/types.ts +6 -0
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/output/README.md +11 -29
- package/src/core/output/index.ts +1 -1
- package/src/core/output/run.ts +0 -35
- package/src/core/output/types.ts +0 -7
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/variables.ts +0 -0
- package/src/core/probe/dry-run-envelope.ts +4 -0
- package/src/core/probe/mass-assignment/classify.ts +175 -0
- package/src/core/probe/mass-assignment/cleanup.ts +52 -0
- package/src/core/probe/mass-assignment/digest.ts +114 -0
- package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
- package/src/core/probe/mass-assignment/regression.ts +141 -0
- package/src/core/probe/mass-assignment/suspects.ts +92 -0
- package/src/core/probe/mass-assignment/types.ts +135 -0
- package/src/core/probe/mass-assignment-probe.ts +23 -1118
- package/src/core/probe/mass-assignment-template.ts +32 -4
- package/src/core/probe/path-discovery.ts +3 -4
- package/src/core/probe/probe-harness.ts +21 -22
- package/src/core/probe/security/baseline.ts +174 -0
- package/src/core/probe/security/classify.ts +341 -0
- package/src/core/probe/security/cleanup.ts +125 -0
- package/src/core/probe/security/detectors.ts +71 -0
- package/src/core/probe/security/digest.ts +104 -0
- package/src/core/probe/security/orchestrator.ts +398 -0
- package/src/core/probe/security/regression.ts +103 -0
- package/src/core/probe/security/types.ts +151 -0
- package/src/core/probe/security-probe-class.ts +8 -2
- package/src/core/probe/security-probe.ts +28 -1449
- package/src/core/probe/shared.ts +26 -0
- package/src/core/probe/webhooks-probe.ts +5 -7
- package/src/core/runner/assertions.ts +1 -1
- package/src/core/runner/executor.ts +3 -18
- package/src/core/runner/form-encode.ts +8 -18
- package/src/core/runner/http-client.ts +38 -1
- package/src/core/runner/preflight-vars.ts +19 -15
- package/src/core/runner/rate-limiter.ts +11 -29
- package/src/core/runner/run-kind.ts +7 -1
- package/src/core/runner/schema-validator.ts +2 -6
- package/src/core/runner/send-request.ts +11 -6
- package/src/core/runner/types.ts +6 -0
- package/src/core/setup-api.ts +53 -15
- package/src/core/severity/index.ts +0 -63
- package/src/core/spec/infer-schema.ts +102 -0
- package/src/core/spec/merge-specs.ts +156 -0
- package/src/core/spec/schema-from-runs.ts +117 -0
- package/src/core/spec/schema-overlay.ts +130 -0
- package/src/core/util/ajv.ts +13 -0
- package/src/core/util/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/db/migrate.ts +2 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -0
- package/src/db/queries/collections.ts +2 -2
- package/src/db/queries/results.ts +88 -0
- package/src/db/queries/runs.ts +56 -2
- package/src/db/queries.ts +3 -0
- package/src/db/schema.ts +7 -7
- package/src/cli/commands/bootstrap.ts +0 -710
- package/src/core/anti-fp/bootstrap.ts +0 -34
- package/src/core/anti-fp/index.ts +0 -33
- package/src/core/anti-fp/registry.ts +0 -44
- package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
- package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
- package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
- package/src/core/anti-fp/types.ts +0 -68
- package/src/core/generator/create-body.ts +0 -89
|
@@ -13,25 +13,15 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { EndpointInfo, SecuritySchemeInfo } from "./types.ts";
|
|
16
|
-
import {
|
|
16
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
17
|
+
import { schemeVarName, resourceVar, computeAmbiguousPathParams, fixtureVarNameForPathParam, owningCollectionForPathParam } from "./suite-generator.ts";
|
|
17
18
|
import type { ApiResourceMap } from "./resources-builder.ts";
|
|
19
|
+
import { canonicalVarName, isFkFixtureField, effectiveObjectShape } from "./data-factory.ts";
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* entry as the spec's path-param spelling (`issue_id`, `audience_id`).
|
|
24
|
-
*
|
|
25
|
-
* Idempotent on already-snake_case input. The HTTP request still sends
|
|
26
|
-
* the raw field name to the server — only the var-name namespace is
|
|
27
|
-
* normalised (see `create-body.ts:substituteFkFields`).
|
|
28
|
-
*/
|
|
29
|
-
export function canonicalVarName(name: string): string {
|
|
30
|
-
return name
|
|
31
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
32
|
-
.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
33
|
-
.toLowerCase();
|
|
34
|
-
}
|
|
21
|
+
// canonicalVarName now lives in data-factory (leaf module, shared with the
|
|
22
|
+
// suite generator). Re-exported here for back-compat: create-body.ts and
|
|
23
|
+
// existing callers still import it from fixtures-builder.
|
|
24
|
+
export { canonicalVarName };
|
|
35
25
|
|
|
36
26
|
export type FixtureSource = "auth" | "server" | "path" | "header" | "body-fk" | "capture-chain";
|
|
37
27
|
|
|
@@ -137,13 +127,22 @@ export function buildApiFixtureManifest(params: BuildFixturesParams): ApiFixture
|
|
|
137
127
|
}
|
|
138
128
|
}
|
|
139
129
|
|
|
140
|
-
// 3. Required path params → one var per unique name
|
|
130
|
+
// 3. Required path params → one var per unique name, disambiguated by
|
|
131
|
+
// owning resource when the raw name is genuinely reused across
|
|
132
|
+
// distinct collections (ARV-369). Without this, a spec where
|
|
133
|
+
// `{code}` appears under both `/macros/v30/{code}` and
|
|
134
|
+
// `/templates/v30/{code}` collapses to ONE `code` fixture — one
|
|
135
|
+
// `.env.yaml` value then silently misapplies to whichever resources
|
|
136
|
+
// don't own it, producing false-negative 404s in generated suites.
|
|
137
|
+
// Single-owner param names (the common case) keep their raw name,
|
|
138
|
+
// unchanged from pre-ARV-369 behavior.
|
|
139
|
+
const ambiguousPathParams = computeAmbiguousPathParams(params.endpoints);
|
|
141
140
|
for (const ep of params.endpoints) {
|
|
142
141
|
for (const p of ep.parameters) {
|
|
143
142
|
if (p.in !== "path") continue;
|
|
144
143
|
if (p.required === false) continue;
|
|
145
|
-
const
|
|
146
|
-
let req = fixtures.get(
|
|
144
|
+
const varName = fixtureVarNameForPathParam(ep.path, p.name, ambiguousPathParams);
|
|
145
|
+
let req = fixtures.get(varName);
|
|
147
146
|
if (!req) {
|
|
148
147
|
const schema = p.schema as { type?: string; format?: string; example?: unknown } | undefined;
|
|
149
148
|
let defaultValue = "";
|
|
@@ -151,15 +150,18 @@ export function buildApiFixtureManifest(params: BuildFixturesParams): ApiFixture
|
|
|
151
150
|
else if (schema?.format === "uuid") defaultValue = "";
|
|
152
151
|
else if (schema?.type === "integer" || schema?.type === "number") defaultValue = "";
|
|
153
152
|
|
|
153
|
+
const scopeNote = ambiguousPathParams.has(p.name)
|
|
154
|
+
? ` Scoped to "${owningCollectionForPathParam(ep.path, p.name) ?? "?"}" — the raw name "${p.name}" is also used by other resources with unrelated ids.`
|
|
155
|
+
: "";
|
|
154
156
|
req = {
|
|
155
|
-
name,
|
|
157
|
+
name: varName,
|
|
156
158
|
source: "path",
|
|
157
|
-
description: `Path parameter ${name}${schema?.format ? ` (${schema.format})` : schema?.type ? ` (${schema.type})` : ""}
|
|
159
|
+
description: `Path parameter ${p.name}${schema?.format ? ` (${schema.format})` : schema?.type ? ` (${schema.type})` : ""}.${scopeNote} Set to a real id from your account, or leave blank to skip dependent tests.`,
|
|
158
160
|
affectedEndpoints: [],
|
|
159
161
|
required: true,
|
|
160
162
|
defaultValue,
|
|
161
163
|
};
|
|
162
|
-
fixtures.set(
|
|
164
|
+
fixtures.set(varName, req);
|
|
163
165
|
}
|
|
164
166
|
pushAffected(req, ep);
|
|
165
167
|
}
|
|
@@ -200,14 +202,19 @@ export function buildApiFixtureManifest(params: BuildFixturesParams): ApiFixture
|
|
|
200
202
|
for (const ep of params.endpoints) {
|
|
201
203
|
const method = ep.method.toUpperCase();
|
|
202
204
|
if (method !== "POST" && method !== "PUT" && method !== "PATCH") continue;
|
|
203
|
-
const schema = ep.requestBodySchema as
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const required =
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
const schema = ep.requestBodySchema as OpenAPIV3.SchemaObject | undefined;
|
|
206
|
+
if (!schema) continue;
|
|
207
|
+
// effectiveObjectShape merges allOf — .NET/Swagger bodies wrap models in
|
|
208
|
+
// allOf, so a direct schema.properties read misses every FK field.
|
|
209
|
+
const { properties, required } = effectiveObjectShape(schema);
|
|
210
|
+
if (Object.keys(properties).length === 0) continue;
|
|
211
|
+
for (const [fieldName, propSchema] of Object.entries(properties)) {
|
|
210
212
|
if (!required.has(fieldName)) continue;
|
|
213
|
+
// ARV-45: catch FK ids AND closed-vocab reference codes
|
|
214
|
+
// (`sequenceTypeCode`) the generator can't synthesise — kept in
|
|
215
|
+
// lockstep with `wireBodyFkRefs` in suite-generator so every var the
|
|
216
|
+
// tests reference appears here (decision-7 manifest contract).
|
|
217
|
+
if (!isFkFixtureField(fieldName, propSchema as OpenAPIV3.SchemaObject)) continue;
|
|
211
218
|
// ARV-138: canonicalise camelCase to snake_case so `issueId` shares
|
|
212
219
|
// a manifest slot with path-param `issue_id`. The raw `fieldName`
|
|
213
220
|
// still goes to the server unchanged via `create-body.ts` —
|
|
@@ -223,7 +230,7 @@ export function buildApiFixtureManifest(params: BuildFixturesParams): ApiFixture
|
|
|
223
230
|
fixtures.set(varName, {
|
|
224
231
|
name: varName,
|
|
225
232
|
source: "body-fk",
|
|
226
|
-
description: `Foreign-key
|
|
233
|
+
description: `Foreign-key / reference field "${fieldName}" consumed by ${epLabel(ep)} request body. Set to a real value from your account, or leave blank to skip dependent tests.`,
|
|
227
234
|
affectedEndpoints: [epLabel(ep)],
|
|
228
235
|
required: true,
|
|
229
236
|
defaultValue: "",
|
|
@@ -12,5 +12,3 @@ export { buildApiResourceMap, serializeApiResourceMap } from "./resources-builde
|
|
|
12
12
|
export type { ApiResourceMap, ApiResourceEntry, ResourceFkRef } from "./resources-builder.ts";
|
|
13
13
|
export { buildApiFixtureManifest, serializeApiFixtureManifest } from "./fixtures-builder.ts";
|
|
14
14
|
export type { ApiFixtureManifest, FixtureRequirement, FixtureSource } from "./fixtures-builder.ts";
|
|
15
|
-
export { buildCreateRequestBody } from "./create-body.ts";
|
|
16
|
-
export type { BuildCreateRequestBodyOptions } from "./create-body.ts";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { rootCertificates } from "node:tls";
|
|
1
3
|
import { dereference } from "@readme/openapi-parser";
|
|
2
4
|
import type { OpenAPIV3 } from "openapi-types";
|
|
3
5
|
import type { EndpointInfo, ResponseInfo, SecuritySchemeInfo } from "./types.ts";
|
|
@@ -5,12 +7,48 @@ import { disambiguateGenericPathParams } from "./path-param-disambig.ts";
|
|
|
5
7
|
|
|
6
8
|
const HTTP_METHODS = ["get", "post", "put", "patch", "delete"] as const;
|
|
7
9
|
|
|
8
|
-
export
|
|
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> {
|
|
9
48
|
// For HTTP URLs, fetch the spec first then dereference the parsed object
|
|
10
49
|
if (specPath.startsWith("http://") || specPath.startsWith("https://")) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
});
|
|
50
|
+
const tls = resolveSpecFetchTls(options);
|
|
51
|
+
const resp = await fetch(specPath, { ...(tls ? { tls } : {}) });
|
|
14
52
|
if (!resp.ok) throw new Error(`Failed to fetch spec: ${resp.status} ${resp.statusText}`);
|
|
15
53
|
const spec = await resp.json();
|
|
16
54
|
const api = await dereference(spec as string);
|
|
@@ -20,6 +58,29 @@ export async function readOpenApiSpec(specPath: string, options?: { insecure?: b
|
|
|
20
58
|
return api as OpenAPIV3.Document;
|
|
21
59
|
}
|
|
22
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
|
+
|
|
23
84
|
export function extractSecuritySchemes(doc: OpenAPIV3.Document): SecuritySchemeInfo[] {
|
|
24
85
|
const schemes: SecuritySchemeInfo[] = [];
|
|
25
86
|
const securitySchemes = doc.components?.securitySchemes;
|
|
@@ -88,6 +149,15 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
88
149
|
}
|
|
89
150
|
}
|
|
90
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
|
+
|
|
91
161
|
// Request body schema + content type
|
|
92
162
|
let requestBodySchema: OpenAPIV3.SchemaObject | undefined;
|
|
93
163
|
let requestBodyContentType: string | undefined;
|
|
@@ -159,6 +229,12 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
159
229
|
responses.some(r => r.statusCode === 412) ||
|
|
160
230
|
parameters.some(p => p.name.toLowerCase() === "if-match" && p.in === "header");
|
|
161
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
|
+
|
|
162
238
|
endpoints.push({
|
|
163
239
|
path,
|
|
164
240
|
method: method.toUpperCase(),
|
|
@@ -173,6 +249,7 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
173
249
|
security,
|
|
174
250
|
deprecated: (operation.deprecated ?? false) || isMarkedDeprecatedInText(operation.summary, operation.description, operation.operationId),
|
|
175
251
|
requiresEtag,
|
|
252
|
+
...(extensions ? { extensions } : {}),
|
|
176
253
|
});
|
|
177
254
|
}
|
|
178
255
|
}
|
|
@@ -184,6 +261,23 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
184
261
|
return disambiguateGenericPathParams(endpoints);
|
|
185
262
|
}
|
|
186
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
|
+
|
|
187
281
|
/** Spec authors often mark endpoints as deprecated in the summary or
|
|
188
282
|
* description text instead of (or in addition to) the `deprecated: true`
|
|
189
283
|
* flag — common across many SaaS and legacy specs. Without this
|
|
@@ -24,12 +24,35 @@
|
|
|
24
24
|
|
|
25
25
|
import type { EndpointInfo } from "./types.ts";
|
|
26
26
|
|
|
27
|
-
const GENERIC_PARAM_NAMES = new Set(["id", "slug", "uuid", "key", "name", "identifier"]);
|
|
27
|
+
const GENERIC_PARAM_NAMES = new Set(["id", "slug", "uuid", "key", "name", "identifier", "code"]);
|
|
28
28
|
|
|
29
|
-
|
|
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 {
|
|
30
47
|
return !!seg && /^\{[^}]+\}$/.test(seg);
|
|
31
48
|
}
|
|
32
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
|
+
|
|
33
56
|
/** English singularization sufficient for resource-collection nouns. */
|
|
34
57
|
function singularize(word: string): string {
|
|
35
58
|
if (word.length > 3 && /ies$/i.test(word)) return word.slice(0, -3) + "y";
|
|
@@ -61,11 +84,14 @@ export function disambiguateGenericPathParams(endpoints: EndpointInfo[]): Endpoi
|
|
|
61
84
|
if (!m) continue;
|
|
62
85
|
const paramName = m[1]!;
|
|
63
86
|
if (!GENERIC_PARAM_NAMES.has(paramName.toLowerCase())) continue;
|
|
64
|
-
// Walk back to nearest non-param non-empty segment as "parent"
|
|
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.
|
|
65
91
|
let parent: string | undefined;
|
|
66
92
|
for (let j = i - 1; j >= 0; j--) {
|
|
67
93
|
const s = segs[j]!;
|
|
68
|
-
if (s && !isParamSeg(s)) {
|
|
94
|
+
if (s && !isParamSeg(s) && !isSkippableSeg(s)) {
|
|
69
95
|
parent = s;
|
|
70
96
|
break;
|
|
71
97
|
}
|