@vibecheckai/cli 3.5.0 → 3.5.2
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/bin/registry.js +214 -237
- package/bin/runners/cli-utils.js +33 -2
- package/bin/runners/context/analyzer.js +52 -1
- package/bin/runners/context/generators/cursor.js +2 -49
- package/bin/runners/context/git-context.js +3 -1
- package/bin/runners/context/team-conventions.js +33 -7
- package/bin/runners/lib/analysis-core.js +25 -5
- package/bin/runners/lib/analyzers.js +431 -481
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/doctor/modules/security.js +3 -1
- package/bin/runners/lib/engine/ast-cache.js +210 -0
- package/bin/runners/lib/engine/auth-extractor.js +211 -0
- package/bin/runners/lib/engine/billing-extractor.js +112 -0
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
- package/bin/runners/lib/engine/env-extractor.js +207 -0
- package/bin/runners/lib/engine/express-extractor.js +208 -0
- package/bin/runners/lib/engine/extractors.js +849 -0
- package/bin/runners/lib/engine/index.js +207 -0
- package/bin/runners/lib/engine/repo-index.js +514 -0
- package/bin/runners/lib/engine/types.js +124 -0
- package/bin/runners/lib/engines/accessibility-engine.js +18 -218
- package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
- package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
- package/bin/runners/lib/engines/mock-data-engine.js +10 -53
- package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
- package/bin/runners/lib/engines/type-aware-engine.js +39 -263
- package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/entitlements-v2.js +73 -97
- package/bin/runners/lib/error-handler.js +44 -3
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +7 -1
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/html-proof-report.js +700 -350
- package/bin/runners/lib/missions/plan.js +6 -46
- package/bin/runners/lib/missions/templates.js +0 -232
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/route-detection.js +137 -68
- package/bin/runners/lib/scan-output.js +91 -76
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -0
- package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
- package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
- package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/validator.js +27 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/ship-output-enterprise.js +23 -23
- package/bin/runners/lib/ship-output.js +75 -31
- package/bin/runners/lib/terminal-ui.js +6 -113
- package/bin/runners/lib/truth.js +351 -10
- package/bin/runners/lib/unified-cli-output.js +430 -603
- package/bin/runners/lib/unified-output.js +13 -9
- package/bin/runners/runAIAgent.js +10 -5
- package/bin/runners/runAgent.js +0 -3
- package/bin/runners/runAllowlist.js +389 -0
- package/bin/runners/runApprove.js +0 -33
- package/bin/runners/runAuth.js +73 -45
- package/bin/runners/runCheckpoint.js +51 -11
- package/bin/runners/runClassify.js +85 -21
- package/bin/runners/runContext.js +0 -3
- package/bin/runners/runDoctor.js +41 -28
- package/bin/runners/runEvidencePack.js +362 -0
- package/bin/runners/runFirewall.js +0 -3
- package/bin/runners/runFirewallHook.js +0 -3
- package/bin/runners/runFix.js +66 -76
- package/bin/runners/runGuard.js +18 -411
- package/bin/runners/runInit.js +113 -30
- package/bin/runners/runLabs.js +424 -0
- package/bin/runners/runMcp.js +19 -25
- package/bin/runners/runPolish.js +64 -240
- package/bin/runners/runPromptFirewall.js +12 -5
- package/bin/runners/runProve.js +57 -22
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +59 -68
- package/bin/runners/runReport.js +38 -33
- package/bin/runners/runRuntime.js +8 -5
- package/bin/runners/runScan.js +1413 -190
- package/bin/runners/runShip.js +113 -719
- package/bin/runners/runTruth.js +0 -3
- package/bin/runners/runValidate.js +13 -9
- package/bin/runners/runWatch.js +23 -14
- package/bin/scan.js +6 -1
- package/bin/vibecheck.js +204 -185
- package/mcp-server/deprecation-middleware.js +282 -0
- package/mcp-server/handlers/index.ts +15 -0
- package/mcp-server/handlers/tool-handler.ts +554 -0
- package/mcp-server/index-v1.js +698 -0
- package/mcp-server/index.js +210 -238
- package/mcp-server/lib/cache-wrapper.cjs +383 -0
- package/mcp-server/lib/error-envelope.js +138 -0
- package/mcp-server/lib/executor.ts +499 -0
- package/mcp-server/lib/index.ts +19 -0
- package/mcp-server/lib/rate-limiter.js +166 -0
- package/mcp-server/lib/sandbox.test.ts +519 -0
- package/mcp-server/lib/sandbox.ts +395 -0
- package/mcp-server/lib/types.ts +267 -0
- package/mcp-server/package.json +12 -3
- package/mcp-server/registry/tool-registry.js +794 -0
- package/mcp-server/registry/tools.json +605 -0
- package/mcp-server/registry.test.ts +334 -0
- package/mcp-server/tests/tier-gating.test.js +297 -0
- package/mcp-server/tier-auth.js +378 -45
- package/mcp-server/tools-v3.js +353 -442
- package/mcp-server/tsconfig.json +37 -0
- package/mcp-server/vibecheck-2.0-tools.js +14 -1
- package/package.json +1 -1
- package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
- package/bin/runners/lib/audit-logger.js +0 -532
- package/bin/runners/lib/authority/authorities/architecture.js +0 -364
- package/bin/runners/lib/authority/authorities/compliance.js +0 -341
- package/bin/runners/lib/authority/authorities/human.js +0 -343
- package/bin/runners/lib/authority/authorities/quality.js +0 -420
- package/bin/runners/lib/authority/authorities/security.js +0 -228
- package/bin/runners/lib/authority/index.js +0 -293
- package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
- package/bin/runners/lib/cli-charts.js +0 -368
- package/bin/runners/lib/cli-config-display.js +0 -405
- package/bin/runners/lib/cli-demo.js +0 -275
- package/bin/runners/lib/cli-errors.js +0 -438
- package/bin/runners/lib/cli-help-formatter.js +0 -439
- package/bin/runners/lib/cli-interactive-menu.js +0 -509
- package/bin/runners/lib/cli-prompts.js +0 -441
- package/bin/runners/lib/cli-scan-cards.js +0 -362
- package/bin/runners/lib/compliance-reporter.js +0 -710
- package/bin/runners/lib/conductor/index.js +0 -671
- package/bin/runners/lib/easy/README.md +0 -123
- package/bin/runners/lib/easy/index.js +0 -140
- package/bin/runners/lib/easy/interactive-wizard.js +0 -788
- package/bin/runners/lib/easy/one-click-firewall.js +0 -564
- package/bin/runners/lib/easy/zero-config-reality.js +0 -714
- package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
- package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
- package/bin/runners/lib/engines/confidence-scoring.js +0 -276
- package/bin/runners/lib/engines/context-detection.js +0 -264
- package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
- package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
- package/bin/runners/lib/engines/env-variables-engine.js +0 -458
- package/bin/runners/lib/engines/error-handling-engine.js +0 -437
- package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
- package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
- package/bin/runners/lib/engines/framework-detection.js +0 -508
- package/bin/runners/lib/engines/import-order-engine.js +0 -429
- package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
- package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
- package/bin/runners/lib/engines/orchestrator.js +0 -334
- package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
- package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
- package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
- package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
- package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
- package/bin/runners/lib/enhanced-features/index.js +0 -305
- package/bin/runners/lib/enhanced-output.js +0 -631
- package/bin/runners/lib/enterprise.js +0 -300
- package/bin/runners/lib/firewall/command-validator.js +0 -351
- package/bin/runners/lib/firewall/config.js +0 -341
- package/bin/runners/lib/firewall/content-validator.js +0 -519
- package/bin/runners/lib/firewall/index.js +0 -101
- package/bin/runners/lib/firewall/path-validator.js +0 -256
- package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
- package/bin/runners/lib/mcp-utils.js +0 -425
- package/bin/runners/lib/output/index.js +0 -1022
- package/bin/runners/lib/policy-engine.js +0 -652
- package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
- package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
- package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
- package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
- package/bin/runners/lib/polish/autofix/index.js +0 -200
- package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
- package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
- package/bin/runners/lib/polish/backend-checks.js +0 -148
- package/bin/runners/lib/polish/documentation-checks.js +0 -111
- package/bin/runners/lib/polish/frontend-checks.js +0 -168
- package/bin/runners/lib/polish/index.js +0 -71
- package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
- package/bin/runners/lib/polish/library-detection.js +0 -175
- package/bin/runners/lib/polish/performance-checks.js +0 -100
- package/bin/runners/lib/polish/security-checks.js +0 -148
- package/bin/runners/lib/polish/utils.js +0 -203
- package/bin/runners/lib/prompt-builder.js +0 -540
- package/bin/runners/lib/proof-certificate.js +0 -634
- package/bin/runners/lib/reality/accessibility-audit.js +0 -946
- package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
- package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
- package/bin/runners/lib/reality/performance-tracker.js +0 -1077
- package/bin/runners/lib/reality/scenario-generator.js +0 -1404
- package/bin/runners/lib/reality/visual-regression.js +0 -852
- package/bin/runners/lib/reality-profiler.js +0 -717
- package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
- package/bin/runners/lib/review/ai-code-review.js +0 -832
- package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
- package/bin/runners/lib/sbom-generator.js +0 -641
- package/bin/runners/lib/scan-output-enhanced.js +0 -512
- package/bin/runners/lib/security/owasp-scanner.js +0 -939
- package/bin/runners/lib/validators/contract-validator.js +0 -283
- package/bin/runners/lib/validators/dead-export-detector.js +0 -279
- package/bin/runners/lib/validators/dep-audit.js +0 -245
- package/bin/runners/lib/validators/env-validator.js +0 -319
- package/bin/runners/lib/validators/index.js +0 -120
- package/bin/runners/lib/validators/license-checker.js +0 -252
- package/bin/runners/lib/validators/route-validator.js +0 -290
- package/bin/runners/runAuthority.js +0 -528
- package/bin/runners/runConductor.js +0 -772
- package/bin/runners/runContainer.js +0 -366
- package/bin/runners/runEasy.js +0 -410
- package/bin/runners/runIaC.js +0 -372
- package/bin/runners/runVibe.js +0 -791
- package/mcp-server/tools.js +0 -495
|
@@ -1,1012 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API Contract Validation Engine
|
|
3
|
-
*
|
|
4
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
-
* COMPETITIVE MOAT FEATURE - Runtime API Contract Verification
|
|
6
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
-
*
|
|
8
|
-
* This engine validates API requests and responses against OpenAPI/Swagger specs
|
|
9
|
-
* during Reality Mode runtime. It catches:
|
|
10
|
-
* - Response schema mismatches
|
|
11
|
-
* - Missing required fields
|
|
12
|
-
* - Type violations
|
|
13
|
-
* - Undocumented endpoints
|
|
14
|
-
* - Request validation failures
|
|
15
|
-
* - Status code mismatches
|
|
16
|
-
*
|
|
17
|
-
* Features:
|
|
18
|
-
* - Auto-discovery of OpenAPI specs
|
|
19
|
-
* - Runtime request/response interception
|
|
20
|
-
* - Detailed violation reporting
|
|
21
|
-
* - Schema drift detection
|
|
22
|
-
* - API coverage tracking
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
"use strict";
|
|
26
|
-
|
|
27
|
-
const fs = require("fs");
|
|
28
|
-
const path = require("path");
|
|
29
|
-
|
|
30
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
-
// OPENAPI SPEC LOCATIONS (auto-discovery)
|
|
32
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
33
|
-
|
|
34
|
-
const SPEC_LOCATIONS = [
|
|
35
|
-
"openapi.json",
|
|
36
|
-
"openapi.yaml",
|
|
37
|
-
"openapi.yml",
|
|
38
|
-
"swagger.json",
|
|
39
|
-
"swagger.yaml",
|
|
40
|
-
"swagger.yml",
|
|
41
|
-
"api/openapi.json",
|
|
42
|
-
"api/swagger.json",
|
|
43
|
-
"docs/openapi.json",
|
|
44
|
-
"docs/swagger.json",
|
|
45
|
-
".vibecheck/contracts/openapi.json",
|
|
46
|
-
"public/api-docs/openapi.json"
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
50
|
-
// JSON SCHEMA VALIDATOR (Pure JS Implementation)
|
|
51
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
52
|
-
|
|
53
|
-
class SchemaValidator {
|
|
54
|
-
constructor() {
|
|
55
|
-
this.errors = [];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Validate data against JSON Schema
|
|
60
|
-
*/
|
|
61
|
-
validate(data, schema, path = "") {
|
|
62
|
-
this.errors = [];
|
|
63
|
-
this._validate(data, schema, path);
|
|
64
|
-
return {
|
|
65
|
-
valid: this.errors.length === 0,
|
|
66
|
-
errors: this.errors
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
_validate(data, schema, path) {
|
|
71
|
-
if (!schema) return;
|
|
72
|
-
|
|
73
|
-
// Handle $ref (simplified - doesn't resolve external refs)
|
|
74
|
-
if (schema.$ref) {
|
|
75
|
-
// Would need to resolve reference here
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Handle allOf, anyOf, oneOf
|
|
80
|
-
if (schema.allOf) {
|
|
81
|
-
for (const subSchema of schema.allOf) {
|
|
82
|
-
this._validate(data, subSchema, path);
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (schema.anyOf) {
|
|
88
|
-
const originalErrors = [...this.errors];
|
|
89
|
-
let anyValid = false;
|
|
90
|
-
|
|
91
|
-
for (const subSchema of schema.anyOf) {
|
|
92
|
-
this.errors = [];
|
|
93
|
-
this._validate(data, subSchema, path);
|
|
94
|
-
if (this.errors.length === 0) {
|
|
95
|
-
anyValid = true;
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!anyValid) {
|
|
101
|
-
this.errors = originalErrors;
|
|
102
|
-
this.errors.push({
|
|
103
|
-
path,
|
|
104
|
-
message: "Value does not match any of the allowed schemas",
|
|
105
|
-
keyword: "anyOf"
|
|
106
|
-
});
|
|
107
|
-
} else {
|
|
108
|
-
this.errors = originalErrors;
|
|
109
|
-
}
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (schema.oneOf) {
|
|
114
|
-
let matchCount = 0;
|
|
115
|
-
const originalErrors = [...this.errors];
|
|
116
|
-
|
|
117
|
-
for (const subSchema of schema.oneOf) {
|
|
118
|
-
this.errors = [];
|
|
119
|
-
this._validate(data, subSchema, path);
|
|
120
|
-
if (this.errors.length === 0) {
|
|
121
|
-
matchCount++;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
this.errors = originalErrors;
|
|
126
|
-
if (matchCount !== 1) {
|
|
127
|
-
this.errors.push({
|
|
128
|
-
path,
|
|
129
|
-
message: `Value must match exactly one schema, matched ${matchCount}`,
|
|
130
|
-
keyword: "oneOf"
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Type validation
|
|
137
|
-
if (schema.type) {
|
|
138
|
-
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
139
|
-
const actualType = this._getType(data);
|
|
140
|
-
|
|
141
|
-
if (!types.includes(actualType) && !(actualType === "integer" && types.includes("number"))) {
|
|
142
|
-
// Handle nullable
|
|
143
|
-
if (data === null && schema.nullable) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
this.errors.push({
|
|
148
|
-
path,
|
|
149
|
-
message: `Expected ${types.join(" or ")}, got ${actualType}`,
|
|
150
|
-
keyword: "type",
|
|
151
|
-
expected: types,
|
|
152
|
-
actual: actualType
|
|
153
|
-
});
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Null check
|
|
159
|
-
if (data === null) {
|
|
160
|
-
if (!schema.nullable && schema.type !== "null") {
|
|
161
|
-
this.errors.push({
|
|
162
|
-
path,
|
|
163
|
-
message: "Value cannot be null",
|
|
164
|
-
keyword: "nullable"
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// String validations
|
|
171
|
-
if (typeof data === "string") {
|
|
172
|
-
if (schema.minLength !== undefined && data.length < schema.minLength) {
|
|
173
|
-
this.errors.push({
|
|
174
|
-
path,
|
|
175
|
-
message: `String must be at least ${schema.minLength} characters`,
|
|
176
|
-
keyword: "minLength"
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (schema.maxLength !== undefined && data.length > schema.maxLength) {
|
|
181
|
-
this.errors.push({
|
|
182
|
-
path,
|
|
183
|
-
message: `String must be at most ${schema.maxLength} characters`,
|
|
184
|
-
keyword: "maxLength"
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (schema.pattern) {
|
|
189
|
-
const regex = new RegExp(schema.pattern);
|
|
190
|
-
if (!regex.test(data)) {
|
|
191
|
-
this.errors.push({
|
|
192
|
-
path,
|
|
193
|
-
message: `String does not match pattern: ${schema.pattern}`,
|
|
194
|
-
keyword: "pattern"
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (schema.format) {
|
|
200
|
-
this._validateFormat(data, schema.format, path);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (schema.enum && !schema.enum.includes(data)) {
|
|
204
|
-
this.errors.push({
|
|
205
|
-
path,
|
|
206
|
-
message: `Value must be one of: ${schema.enum.join(", ")}`,
|
|
207
|
-
keyword: "enum"
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Number validations
|
|
213
|
-
if (typeof data === "number") {
|
|
214
|
-
if (schema.minimum !== undefined) {
|
|
215
|
-
const valid = schema.exclusiveMinimum ? data > schema.minimum : data >= schema.minimum;
|
|
216
|
-
if (!valid) {
|
|
217
|
-
this.errors.push({
|
|
218
|
-
path,
|
|
219
|
-
message: `Number must be ${schema.exclusiveMinimum ? ">" : ">="} ${schema.minimum}`,
|
|
220
|
-
keyword: "minimum"
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (schema.maximum !== undefined) {
|
|
226
|
-
const valid = schema.exclusiveMaximum ? data < schema.maximum : data <= schema.maximum;
|
|
227
|
-
if (!valid) {
|
|
228
|
-
this.errors.push({
|
|
229
|
-
path,
|
|
230
|
-
message: `Number must be ${schema.exclusiveMaximum ? "<" : "<="} ${schema.maximum}`,
|
|
231
|
-
keyword: "maximum"
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (schema.multipleOf !== undefined && data % schema.multipleOf !== 0) {
|
|
237
|
-
this.errors.push({
|
|
238
|
-
path,
|
|
239
|
-
message: `Number must be a multiple of ${schema.multipleOf}`,
|
|
240
|
-
keyword: "multipleOf"
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Array validations
|
|
246
|
-
if (Array.isArray(data)) {
|
|
247
|
-
if (schema.minItems !== undefined && data.length < schema.minItems) {
|
|
248
|
-
this.errors.push({
|
|
249
|
-
path,
|
|
250
|
-
message: `Array must have at least ${schema.minItems} items`,
|
|
251
|
-
keyword: "minItems"
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (schema.maxItems !== undefined && data.length > schema.maxItems) {
|
|
256
|
-
this.errors.push({
|
|
257
|
-
path,
|
|
258
|
-
message: `Array must have at most ${schema.maxItems} items`,
|
|
259
|
-
keyword: "maxItems"
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (schema.uniqueItems) {
|
|
264
|
-
const seen = new Set();
|
|
265
|
-
for (let i = 0; i < data.length; i++) {
|
|
266
|
-
const key = JSON.stringify(data[i]);
|
|
267
|
-
if (seen.has(key)) {
|
|
268
|
-
this.errors.push({
|
|
269
|
-
path,
|
|
270
|
-
message: "Array items must be unique",
|
|
271
|
-
keyword: "uniqueItems"
|
|
272
|
-
});
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
seen.add(key);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (schema.items) {
|
|
280
|
-
for (let i = 0; i < data.length; i++) {
|
|
281
|
-
this._validate(data[i], schema.items, `${path}[${i}]`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Object validations
|
|
287
|
-
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
|
|
288
|
-
const properties = schema.properties || {};
|
|
289
|
-
const required = schema.required || [];
|
|
290
|
-
const additionalProperties = schema.additionalProperties;
|
|
291
|
-
|
|
292
|
-
// Check required properties
|
|
293
|
-
for (const prop of required) {
|
|
294
|
-
if (!(prop in data)) {
|
|
295
|
-
this.errors.push({
|
|
296
|
-
path: path ? `${path}.${prop}` : prop,
|
|
297
|
-
message: `Missing required property: ${prop}`,
|
|
298
|
-
keyword: "required"
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Validate properties
|
|
304
|
-
for (const [key, value] of Object.entries(data)) {
|
|
305
|
-
const propPath = path ? `${path}.${key}` : key;
|
|
306
|
-
|
|
307
|
-
if (properties[key]) {
|
|
308
|
-
this._validate(value, properties[key], propPath);
|
|
309
|
-
} else if (additionalProperties === false) {
|
|
310
|
-
this.errors.push({
|
|
311
|
-
path: propPath,
|
|
312
|
-
message: `Unexpected property: ${key}`,
|
|
313
|
-
keyword: "additionalProperties"
|
|
314
|
-
});
|
|
315
|
-
} else if (typeof additionalProperties === "object") {
|
|
316
|
-
this._validate(value, additionalProperties, propPath);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Check property count
|
|
321
|
-
const propCount = Object.keys(data).length;
|
|
322
|
-
if (schema.minProperties !== undefined && propCount < schema.minProperties) {
|
|
323
|
-
this.errors.push({
|
|
324
|
-
path,
|
|
325
|
-
message: `Object must have at least ${schema.minProperties} properties`,
|
|
326
|
-
keyword: "minProperties"
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (schema.maxProperties !== undefined && propCount > schema.maxProperties) {
|
|
331
|
-
this.errors.push({
|
|
332
|
-
path,
|
|
333
|
-
message: `Object must have at most ${schema.maxProperties} properties`,
|
|
334
|
-
keyword: "maxProperties"
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
_getType(value) {
|
|
341
|
-
if (value === null) return "null";
|
|
342
|
-
if (Array.isArray(value)) return "array";
|
|
343
|
-
if (Number.isInteger(value)) return "integer";
|
|
344
|
-
return typeof value;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
_validateFormat(data, format, path) {
|
|
348
|
-
const formats = {
|
|
349
|
-
"date-time": /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/,
|
|
350
|
-
"date": /^\d{4}-\d{2}-\d{2}$/,
|
|
351
|
-
"time": /^\d{2}:\d{2}:\d{2}(\.\d+)?$/,
|
|
352
|
-
"email": /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
353
|
-
"uri": /^https?:\/\/.+/,
|
|
354
|
-
"uuid": /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
355
|
-
"ipv4": /^(\d{1,3}\.){3}\d{1,3}$/,
|
|
356
|
-
"ipv6": /^([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}$/i
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
if (formats[format] && !formats[format].test(data)) {
|
|
360
|
-
this.errors.push({
|
|
361
|
-
path,
|
|
362
|
-
message: `String does not match format: ${format}`,
|
|
363
|
-
keyword: "format"
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
370
|
-
// API CONTRACT VALIDATOR CLASS
|
|
371
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
372
|
-
|
|
373
|
-
class APIContractValidator {
|
|
374
|
-
constructor(options = {}) {
|
|
375
|
-
this.projectRoot = options.projectRoot || process.cwd();
|
|
376
|
-
this.spec = null;
|
|
377
|
-
this.specPath = options.specPath || null;
|
|
378
|
-
this.strictMode = options.strictMode || false;
|
|
379
|
-
this.ignorePatterns = options.ignorePatterns || [];
|
|
380
|
-
this.schemaValidator = new SchemaValidator();
|
|
381
|
-
this.violations = [];
|
|
382
|
-
this.coverage = {
|
|
383
|
-
endpoints: new Map(),
|
|
384
|
-
statusCodes: new Map()
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Load OpenAPI specification
|
|
390
|
-
*/
|
|
391
|
-
async loadSpec(specPath = null) {
|
|
392
|
-
const pathToLoad = specPath || this.specPath || this.findSpec();
|
|
393
|
-
|
|
394
|
-
if (!pathToLoad) {
|
|
395
|
-
return {
|
|
396
|
-
loaded: false,
|
|
397
|
-
error: "No OpenAPI/Swagger specification found"
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
const content = fs.readFileSync(pathToLoad, "utf8");
|
|
403
|
-
|
|
404
|
-
// Parse YAML or JSON
|
|
405
|
-
if (pathToLoad.endsWith(".yaml") || pathToLoad.endsWith(".yml")) {
|
|
406
|
-
// Simple YAML parsing (would use js-yaml in production)
|
|
407
|
-
this.spec = this.parseYaml(content);
|
|
408
|
-
} else {
|
|
409
|
-
this.spec = JSON.parse(content);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
this.specPath = pathToLoad;
|
|
413
|
-
this.resolveRefs();
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
loaded: true,
|
|
417
|
-
path: pathToLoad,
|
|
418
|
-
info: this.spec.info,
|
|
419
|
-
endpointCount: this.countEndpoints()
|
|
420
|
-
};
|
|
421
|
-
} catch (error) {
|
|
422
|
-
return {
|
|
423
|
-
loaded: false,
|
|
424
|
-
error: error.message,
|
|
425
|
-
path: pathToLoad
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Find OpenAPI spec in project
|
|
432
|
-
*/
|
|
433
|
-
findSpec() {
|
|
434
|
-
for (const location of SPEC_LOCATIONS) {
|
|
435
|
-
const fullPath = path.join(this.projectRoot, location);
|
|
436
|
-
if (fs.existsSync(fullPath)) {
|
|
437
|
-
return fullPath;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return null;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Simple YAML parser (subset)
|
|
445
|
-
*/
|
|
446
|
-
parseYaml(content) {
|
|
447
|
-
// For production, use js-yaml library
|
|
448
|
-
// This is a simplified parser for common cases
|
|
449
|
-
try {
|
|
450
|
-
// Try JSON first (YAML is a superset of JSON)
|
|
451
|
-
return JSON.parse(content);
|
|
452
|
-
} catch {
|
|
453
|
-
// Basic YAML parsing
|
|
454
|
-
// This would need a proper YAML library
|
|
455
|
-
throw new Error("YAML parsing requires js-yaml library");
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Resolve $ref references in spec
|
|
461
|
-
*/
|
|
462
|
-
resolveRefs() {
|
|
463
|
-
if (!this.spec) return;
|
|
464
|
-
|
|
465
|
-
const components = this.spec.components || this.spec.definitions || {};
|
|
466
|
-
|
|
467
|
-
const resolveObject = (obj) => {
|
|
468
|
-
if (!obj || typeof obj !== "object") return obj;
|
|
469
|
-
|
|
470
|
-
if (obj.$ref) {
|
|
471
|
-
const refPath = obj.$ref.replace("#/components/schemas/", "")
|
|
472
|
-
.replace("#/definitions/", "");
|
|
473
|
-
const resolved = components.schemas?.[refPath] || components[refPath];
|
|
474
|
-
if (resolved) {
|
|
475
|
-
return resolveObject({ ...resolved });
|
|
476
|
-
}
|
|
477
|
-
return obj;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (Array.isArray(obj)) {
|
|
481
|
-
return obj.map(resolveObject);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const result = {};
|
|
485
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
486
|
-
result[key] = resolveObject(value);
|
|
487
|
-
}
|
|
488
|
-
return result;
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
this.spec = resolveObject(this.spec);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Count endpoints in spec
|
|
496
|
-
*/
|
|
497
|
-
countEndpoints() {
|
|
498
|
-
if (!this.spec?.paths) return 0;
|
|
499
|
-
|
|
500
|
-
let count = 0;
|
|
501
|
-
for (const path of Object.values(this.spec.paths)) {
|
|
502
|
-
count += Object.keys(path).filter(m =>
|
|
503
|
-
["get", "post", "put", "delete", "patch", "options", "head"].includes(m)
|
|
504
|
-
).length;
|
|
505
|
-
}
|
|
506
|
-
return count;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
510
|
-
// REQUEST/RESPONSE VALIDATION
|
|
511
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Validate an API request
|
|
515
|
-
*/
|
|
516
|
-
validateRequest(request) {
|
|
517
|
-
if (!this.spec) {
|
|
518
|
-
return { valid: true, errors: [], skipped: true, reason: "No spec loaded" };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const { method, url, body, headers, queryParams } = request;
|
|
522
|
-
const errors = [];
|
|
523
|
-
|
|
524
|
-
// Match endpoint
|
|
525
|
-
const match = this.matchEndpoint(method, url);
|
|
526
|
-
|
|
527
|
-
if (!match) {
|
|
528
|
-
if (this.strictMode) {
|
|
529
|
-
errors.push({
|
|
530
|
-
type: "undocumented_endpoint",
|
|
531
|
-
severity: "WARN",
|
|
532
|
-
message: `Undocumented endpoint: ${method.toUpperCase()} ${url}`,
|
|
533
|
-
details: { method, url }
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
return { valid: errors.length === 0, errors };
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const { operation, pathParams } = match;
|
|
540
|
-
|
|
541
|
-
// Track coverage
|
|
542
|
-
this.trackCoverage(method, match.pathPattern);
|
|
543
|
-
|
|
544
|
-
// Validate path parameters
|
|
545
|
-
if (operation.parameters) {
|
|
546
|
-
for (const param of operation.parameters.filter(p => p.in === "path")) {
|
|
547
|
-
if (param.required && !pathParams[param.name]) {
|
|
548
|
-
errors.push({
|
|
549
|
-
type: "missing_path_param",
|
|
550
|
-
severity: "BLOCK",
|
|
551
|
-
message: `Missing required path parameter: ${param.name}`,
|
|
552
|
-
details: { parameter: param.name }
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Validate query parameters
|
|
559
|
-
if (operation.parameters && queryParams) {
|
|
560
|
-
for (const param of operation.parameters.filter(p => p.in === "query")) {
|
|
561
|
-
if (param.required && !(param.name in queryParams)) {
|
|
562
|
-
errors.push({
|
|
563
|
-
type: "missing_query_param",
|
|
564
|
-
severity: "BLOCK",
|
|
565
|
-
message: `Missing required query parameter: ${param.name}`,
|
|
566
|
-
details: { parameter: param.name }
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (param.name in queryParams && param.schema) {
|
|
571
|
-
const validation = this.schemaValidator.validate(
|
|
572
|
-
queryParams[param.name],
|
|
573
|
-
param.schema,
|
|
574
|
-
`query.${param.name}`
|
|
575
|
-
);
|
|
576
|
-
|
|
577
|
-
if (!validation.valid) {
|
|
578
|
-
errors.push({
|
|
579
|
-
type: "invalid_query_param",
|
|
580
|
-
severity: "WARN",
|
|
581
|
-
message: `Invalid query parameter: ${param.name}`,
|
|
582
|
-
details: { parameter: param.name, errors: validation.errors }
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Validate request body
|
|
590
|
-
if (body && operation.requestBody) {
|
|
591
|
-
const contentType = headers?.["content-type"] || "application/json";
|
|
592
|
-
const mediaType = operation.requestBody.content?.[contentType] ||
|
|
593
|
-
operation.requestBody.content?.["application/json"];
|
|
594
|
-
|
|
595
|
-
if (mediaType?.schema) {
|
|
596
|
-
let bodyData = body;
|
|
597
|
-
if (typeof body === "string") {
|
|
598
|
-
try {
|
|
599
|
-
bodyData = JSON.parse(body);
|
|
600
|
-
} catch {
|
|
601
|
-
errors.push({
|
|
602
|
-
type: "invalid_json",
|
|
603
|
-
severity: "BLOCK",
|
|
604
|
-
message: "Request body is not valid JSON"
|
|
605
|
-
});
|
|
606
|
-
return { valid: false, errors };
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const validation = this.schemaValidator.validate(bodyData, mediaType.schema, "body");
|
|
611
|
-
|
|
612
|
-
if (!validation.valid) {
|
|
613
|
-
for (const error of validation.errors) {
|
|
614
|
-
errors.push({
|
|
615
|
-
type: "request_schema_violation",
|
|
616
|
-
severity: "BLOCK",
|
|
617
|
-
message: `Request body validation failed: ${error.message}`,
|
|
618
|
-
details: { path: error.path, keyword: error.keyword }
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
} else if (operation.requestBody?.required && !body) {
|
|
624
|
-
errors.push({
|
|
625
|
-
type: "missing_request_body",
|
|
626
|
-
severity: "BLOCK",
|
|
627
|
-
message: "Request body is required"
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
return { valid: errors.length === 0, errors, match };
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Validate an API response
|
|
636
|
-
*/
|
|
637
|
-
validateResponse(response, requestMatch = null) {
|
|
638
|
-
if (!this.spec) {
|
|
639
|
-
return { valid: true, errors: [], skipped: true, reason: "No spec loaded" };
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const { status, body, headers, method, url } = response;
|
|
643
|
-
const errors = [];
|
|
644
|
-
|
|
645
|
-
// Get match from request or find it
|
|
646
|
-
let match = requestMatch;
|
|
647
|
-
if (!match && method && url) {
|
|
648
|
-
match = this.matchEndpoint(method, url);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (!match) {
|
|
652
|
-
if (this.strictMode) {
|
|
653
|
-
errors.push({
|
|
654
|
-
type: "undocumented_endpoint",
|
|
655
|
-
severity: "WARN",
|
|
656
|
-
message: `Response from undocumented endpoint: ${method?.toUpperCase()} ${url}`,
|
|
657
|
-
details: { method, url, status }
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
return { valid: errors.length === 0, errors };
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const { operation, pathPattern } = match;
|
|
664
|
-
|
|
665
|
-
// Track status code coverage
|
|
666
|
-
this.trackStatusCode(pathPattern, method, status);
|
|
667
|
-
|
|
668
|
-
// Check if status code is documented
|
|
669
|
-
const responses = operation.responses || {};
|
|
670
|
-
const statusStr = status.toString();
|
|
671
|
-
const responseSpec = responses[statusStr] ||
|
|
672
|
-
responses[`${Math.floor(status / 100)}XX`] ||
|
|
673
|
-
responses.default;
|
|
674
|
-
|
|
675
|
-
if (!responseSpec) {
|
|
676
|
-
errors.push({
|
|
677
|
-
type: "undocumented_status",
|
|
678
|
-
severity: "WARN",
|
|
679
|
-
message: `Undocumented status code: ${status} for ${method?.toUpperCase()} ${pathPattern}`,
|
|
680
|
-
details: { status, documentedStatuses: Object.keys(responses) }
|
|
681
|
-
});
|
|
682
|
-
return { valid: errors.length === 0, errors };
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Validate response body
|
|
686
|
-
if (body && responseSpec.content) {
|
|
687
|
-
const contentType = headers?.["content-type"]?.split(";")[0] || "application/json";
|
|
688
|
-
const mediaType = responseSpec.content[contentType] ||
|
|
689
|
-
responseSpec.content["application/json"] ||
|
|
690
|
-
responseSpec.content["*/*"];
|
|
691
|
-
|
|
692
|
-
if (mediaType?.schema) {
|
|
693
|
-
let bodyData = body;
|
|
694
|
-
if (typeof body === "string") {
|
|
695
|
-
try {
|
|
696
|
-
bodyData = JSON.parse(body);
|
|
697
|
-
} catch {
|
|
698
|
-
errors.push({
|
|
699
|
-
type: "invalid_json",
|
|
700
|
-
severity: "WARN",
|
|
701
|
-
message: "Response body is not valid JSON"
|
|
702
|
-
});
|
|
703
|
-
return { valid: false, errors };
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const validation = this.schemaValidator.validate(bodyData, mediaType.schema, "response");
|
|
708
|
-
|
|
709
|
-
if (!validation.valid) {
|
|
710
|
-
for (const error of validation.errors) {
|
|
711
|
-
errors.push({
|
|
712
|
-
type: "response_schema_violation",
|
|
713
|
-
severity: "BLOCK",
|
|
714
|
-
message: `Response validation failed: ${error.message}`,
|
|
715
|
-
details: {
|
|
716
|
-
path: error.path,
|
|
717
|
-
keyword: error.keyword,
|
|
718
|
-
status,
|
|
719
|
-
endpoint: `${method?.toUpperCase()} ${pathPattern}`
|
|
720
|
-
}
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Check for expected error responses
|
|
728
|
-
if (status >= 400 && status < 500) {
|
|
729
|
-
// Client errors should have proper structure
|
|
730
|
-
const bodyData = typeof body === "string" ? JSON.parse(body) : body;
|
|
731
|
-
if (bodyData && !bodyData.error && !bodyData.message && !bodyData.errors) {
|
|
732
|
-
errors.push({
|
|
733
|
-
type: "poor_error_response",
|
|
734
|
-
severity: "WARN",
|
|
735
|
-
message: `Error response lacks standard error structure`,
|
|
736
|
-
details: { status, hasError: !!bodyData.error, hasMessage: !!bodyData.message }
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
return { valid: errors.length === 0, errors, match };
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* Match URL to endpoint in spec
|
|
746
|
-
*/
|
|
747
|
-
matchEndpoint(method, url) {
|
|
748
|
-
if (!this.spec?.paths) return null;
|
|
749
|
-
|
|
750
|
-
const urlObj = new URL(url, "http://localhost");
|
|
751
|
-
const pathname = urlObj.pathname;
|
|
752
|
-
const methodLower = method.toLowerCase();
|
|
753
|
-
|
|
754
|
-
for (const [pattern, pathItem] of Object.entries(this.spec.paths)) {
|
|
755
|
-
if (!pathItem[methodLower]) continue;
|
|
756
|
-
|
|
757
|
-
const match = this.matchPath(pathname, pattern);
|
|
758
|
-
if (match) {
|
|
759
|
-
return {
|
|
760
|
-
pathPattern: pattern,
|
|
761
|
-
operation: pathItem[methodLower],
|
|
762
|
-
pathParams: match.params
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
return null;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Match URL path against pattern
|
|
772
|
-
*/
|
|
773
|
-
matchPath(pathname, pattern) {
|
|
774
|
-
// Convert OpenAPI pattern to regex
|
|
775
|
-
// /users/{id} -> /users/([^/]+)
|
|
776
|
-
const paramNames = [];
|
|
777
|
-
const regexPattern = pattern.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
778
|
-
paramNames.push(name);
|
|
779
|
-
return "([^/]+)";
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
783
|
-
const match = pathname.match(regex);
|
|
784
|
-
|
|
785
|
-
if (!match) return null;
|
|
786
|
-
|
|
787
|
-
const params = {};
|
|
788
|
-
paramNames.forEach((name, i) => {
|
|
789
|
-
params[name] = match[i + 1];
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
return { params };
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
796
|
-
// COVERAGE TRACKING
|
|
797
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* Track endpoint coverage
|
|
801
|
-
*/
|
|
802
|
-
trackCoverage(method, pathPattern) {
|
|
803
|
-
const key = `${method.toUpperCase()} ${pathPattern}`;
|
|
804
|
-
const current = this.coverage.endpoints.get(key) || { count: 0, lastSeen: null };
|
|
805
|
-
|
|
806
|
-
this.coverage.endpoints.set(key, {
|
|
807
|
-
count: current.count + 1,
|
|
808
|
-
lastSeen: new Date().toISOString()
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* Track status code coverage
|
|
814
|
-
*/
|
|
815
|
-
trackStatusCode(pathPattern, method, status) {
|
|
816
|
-
const key = `${method.toUpperCase()} ${pathPattern}`;
|
|
817
|
-
const current = this.coverage.statusCodes.get(key) || new Set();
|
|
818
|
-
current.add(status);
|
|
819
|
-
this.coverage.statusCodes.set(key, current);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Get coverage report
|
|
824
|
-
*/
|
|
825
|
-
getCoverageReport() {
|
|
826
|
-
if (!this.spec?.paths) {
|
|
827
|
-
return { available: false };
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const documented = new Set();
|
|
831
|
-
const documentedStatusCodes = new Map();
|
|
832
|
-
|
|
833
|
-
// Collect all documented endpoints
|
|
834
|
-
for (const [pattern, pathItem] of Object.entries(this.spec.paths)) {
|
|
835
|
-
for (const method of ["get", "post", "put", "delete", "patch"]) {
|
|
836
|
-
if (pathItem[method]) {
|
|
837
|
-
const key = `${method.toUpperCase()} ${pattern}`;
|
|
838
|
-
documented.add(key);
|
|
839
|
-
|
|
840
|
-
const responses = pathItem[method].responses || {};
|
|
841
|
-
documentedStatusCodes.set(key, Object.keys(responses).filter(s => s !== "default"));
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Calculate coverage
|
|
847
|
-
const testedEndpoints = new Set(this.coverage.endpoints.keys());
|
|
848
|
-
const coveredEndpoints = [...documented].filter(e => testedEndpoints.has(e));
|
|
849
|
-
|
|
850
|
-
const coverage = {
|
|
851
|
-
endpoints: {
|
|
852
|
-
total: documented.size,
|
|
853
|
-
covered: coveredEndpoints.length,
|
|
854
|
-
percentage: documented.size > 0
|
|
855
|
-
? ((coveredEndpoints.length / documented.size) * 100).toFixed(1)
|
|
856
|
-
: 0,
|
|
857
|
-
missing: [...documented].filter(e => !testedEndpoints.has(e)),
|
|
858
|
-
undocumented: [...testedEndpoints].filter(e => !documented.has(e))
|
|
859
|
-
},
|
|
860
|
-
statusCodes: {
|
|
861
|
-
byEndpoint: {}
|
|
862
|
-
}
|
|
863
|
-
};
|
|
864
|
-
|
|
865
|
-
// Status code coverage per endpoint
|
|
866
|
-
for (const [endpoint, statuses] of this.coverage.statusCodes) {
|
|
867
|
-
const documented = documentedStatusCodes.get(endpoint) || [];
|
|
868
|
-
const testedStatuses = [...statuses].map(String);
|
|
869
|
-
|
|
870
|
-
coverage.statusCodes.byEndpoint[endpoint] = {
|
|
871
|
-
documented,
|
|
872
|
-
tested: testedStatuses,
|
|
873
|
-
missing: documented.filter(s => !testedStatuses.includes(s))
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
return coverage;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
881
|
-
// INTEGRATION WITH REALITY MODE
|
|
882
|
-
// ═══════════════════════════════════════════════════════════════
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* Create Playwright network interceptor
|
|
886
|
-
*/
|
|
887
|
-
createInterceptor() {
|
|
888
|
-
const self = this;
|
|
889
|
-
|
|
890
|
-
return async (route, request) => {
|
|
891
|
-
const url = request.url();
|
|
892
|
-
const method = request.method();
|
|
893
|
-
|
|
894
|
-
// Skip non-API requests
|
|
895
|
-
if (!url.includes("/api/") && !self.isApiRequest(url)) {
|
|
896
|
-
await route.continue();
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// Validate request
|
|
901
|
-
const requestBody = request.postData();
|
|
902
|
-
const requestValidation = self.validateRequest({
|
|
903
|
-
method,
|
|
904
|
-
url,
|
|
905
|
-
body: requestBody,
|
|
906
|
-
headers: request.headers(),
|
|
907
|
-
queryParams: Object.fromEntries(new URL(url).searchParams)
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
// Continue and capture response
|
|
911
|
-
const response = await route.fetch();
|
|
912
|
-
const responseBody = await response.text();
|
|
913
|
-
|
|
914
|
-
// Validate response
|
|
915
|
-
const responseValidation = self.validateResponse({
|
|
916
|
-
status: response.status(),
|
|
917
|
-
body: responseBody,
|
|
918
|
-
headers: Object.fromEntries(response.headers()),
|
|
919
|
-
method,
|
|
920
|
-
url
|
|
921
|
-
}, requestValidation.match);
|
|
922
|
-
|
|
923
|
-
// Collect violations
|
|
924
|
-
if (!requestValidation.valid) {
|
|
925
|
-
self.violations.push(...requestValidation.errors.map(e => ({
|
|
926
|
-
...e,
|
|
927
|
-
request: { method, url }
|
|
928
|
-
})));
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
if (!responseValidation.valid) {
|
|
932
|
-
self.violations.push(...responseValidation.errors.map(e => ({
|
|
933
|
-
...e,
|
|
934
|
-
request: { method, url },
|
|
935
|
-
response: { status: response.status() }
|
|
936
|
-
})));
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Fulfill with original response
|
|
940
|
-
await route.fulfill({
|
|
941
|
-
status: response.status(),
|
|
942
|
-
headers: Object.fromEntries(response.headers()),
|
|
943
|
-
body: responseBody
|
|
944
|
-
});
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/**
|
|
949
|
-
* Check if URL is an API request
|
|
950
|
-
*/
|
|
951
|
-
isApiRequest(url) {
|
|
952
|
-
try {
|
|
953
|
-
const urlObj = new URL(url);
|
|
954
|
-
const patterns = [
|
|
955
|
-
/\/api\//,
|
|
956
|
-
/\/v\d+\//,
|
|
957
|
-
/\/graphql/,
|
|
958
|
-
/\.(json)$/
|
|
959
|
-
];
|
|
960
|
-
return patterns.some(p => p.test(urlObj.pathname));
|
|
961
|
-
} catch {
|
|
962
|
-
return false;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
/**
|
|
967
|
-
* Get violations report
|
|
968
|
-
*/
|
|
969
|
-
getViolationsReport() {
|
|
970
|
-
const byType = {};
|
|
971
|
-
const bySeverity = { BLOCK: [], WARN: [], INFO: [] };
|
|
972
|
-
|
|
973
|
-
for (const violation of this.violations) {
|
|
974
|
-
const type = violation.type;
|
|
975
|
-
if (!byType[type]) {
|
|
976
|
-
byType[type] = [];
|
|
977
|
-
}
|
|
978
|
-
byType[type].push(violation);
|
|
979
|
-
|
|
980
|
-
const severity = violation.severity || "WARN";
|
|
981
|
-
bySeverity[severity]?.push(violation);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
return {
|
|
985
|
-
total: this.violations.length,
|
|
986
|
-
byType,
|
|
987
|
-
bySeverity,
|
|
988
|
-
blockers: bySeverity.BLOCK.length,
|
|
989
|
-
warnings: bySeverity.WARN.length,
|
|
990
|
-
violations: this.violations
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
/**
|
|
995
|
-
* Reset state
|
|
996
|
-
*/
|
|
997
|
-
reset() {
|
|
998
|
-
this.violations = [];
|
|
999
|
-
this.coverage.endpoints.clear();
|
|
1000
|
-
this.coverage.statusCodes.clear();
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1005
|
-
// EXPORTS
|
|
1006
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1007
|
-
|
|
1008
|
-
module.exports = {
|
|
1009
|
-
APIContractValidator,
|
|
1010
|
-
SchemaValidator,
|
|
1011
|
-
SPEC_LOCATIONS
|
|
1012
|
-
};
|