@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.
Files changed (224) hide show
  1. package/bin/registry.js +214 -237
  2. package/bin/runners/cli-utils.js +33 -2
  3. package/bin/runners/context/analyzer.js +52 -1
  4. package/bin/runners/context/generators/cursor.js +2 -49
  5. package/bin/runners/context/git-context.js +3 -1
  6. package/bin/runners/context/team-conventions.js +33 -7
  7. package/bin/runners/lib/analysis-core.js +25 -5
  8. package/bin/runners/lib/analyzers.js +431 -481
  9. package/bin/runners/lib/default-config.js +127 -0
  10. package/bin/runners/lib/doctor/modules/security.js +3 -1
  11. package/bin/runners/lib/engine/ast-cache.js +210 -0
  12. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  13. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  14. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  15. package/bin/runners/lib/engine/env-extractor.js +207 -0
  16. package/bin/runners/lib/engine/express-extractor.js +208 -0
  17. package/bin/runners/lib/engine/extractors.js +849 -0
  18. package/bin/runners/lib/engine/index.js +207 -0
  19. package/bin/runners/lib/engine/repo-index.js +514 -0
  20. package/bin/runners/lib/engine/types.js +124 -0
  21. package/bin/runners/lib/engines/accessibility-engine.js +18 -218
  22. package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
  23. package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
  24. package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
  25. package/bin/runners/lib/engines/mock-data-engine.js +10 -53
  26. package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
  27. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
  28. package/bin/runners/lib/engines/type-aware-engine.js +39 -263
  29. package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
  30. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  31. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  32. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  33. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  34. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  35. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  36. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  37. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
  38. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  39. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  40. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  41. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  42. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  43. package/bin/runners/lib/entitlements-v2.js +73 -97
  44. package/bin/runners/lib/error-handler.js +44 -3
  45. package/bin/runners/lib/error-messages.js +289 -0
  46. package/bin/runners/lib/evidence-pack.js +7 -1
  47. package/bin/runners/lib/finding-id.js +69 -0
  48. package/bin/runners/lib/finding-sorter.js +89 -0
  49. package/bin/runners/lib/html-proof-report.js +700 -350
  50. package/bin/runners/lib/missions/plan.js +6 -46
  51. package/bin/runners/lib/missions/templates.js +0 -232
  52. package/bin/runners/lib/next-action.js +560 -0
  53. package/bin/runners/lib/prerequisites.js +149 -0
  54. package/bin/runners/lib/route-detection.js +137 -68
  55. package/bin/runners/lib/scan-output.js +91 -76
  56. package/bin/runners/lib/scan-runner.js +135 -0
  57. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  58. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  59. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  60. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  61. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  62. package/bin/runners/lib/schemas/validator.js +27 -0
  63. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  64. package/bin/runners/lib/ship-output-enterprise.js +23 -23
  65. package/bin/runners/lib/ship-output.js +75 -31
  66. package/bin/runners/lib/terminal-ui.js +6 -113
  67. package/bin/runners/lib/truth.js +351 -10
  68. package/bin/runners/lib/unified-cli-output.js +430 -603
  69. package/bin/runners/lib/unified-output.js +13 -9
  70. package/bin/runners/runAIAgent.js +10 -5
  71. package/bin/runners/runAgent.js +0 -3
  72. package/bin/runners/runAllowlist.js +389 -0
  73. package/bin/runners/runApprove.js +0 -33
  74. package/bin/runners/runAuth.js +73 -45
  75. package/bin/runners/runCheckpoint.js +51 -11
  76. package/bin/runners/runClassify.js +85 -21
  77. package/bin/runners/runContext.js +0 -3
  78. package/bin/runners/runDoctor.js +41 -28
  79. package/bin/runners/runEvidencePack.js +362 -0
  80. package/bin/runners/runFirewall.js +0 -3
  81. package/bin/runners/runFirewallHook.js +0 -3
  82. package/bin/runners/runFix.js +66 -76
  83. package/bin/runners/runGuard.js +18 -411
  84. package/bin/runners/runInit.js +113 -30
  85. package/bin/runners/runLabs.js +424 -0
  86. package/bin/runners/runMcp.js +19 -25
  87. package/bin/runners/runPolish.js +64 -240
  88. package/bin/runners/runPromptFirewall.js +12 -5
  89. package/bin/runners/runProve.js +57 -22
  90. package/bin/runners/runQuickstart.js +531 -0
  91. package/bin/runners/runReality.js +59 -68
  92. package/bin/runners/runReport.js +38 -33
  93. package/bin/runners/runRuntime.js +8 -5
  94. package/bin/runners/runScan.js +1413 -190
  95. package/bin/runners/runShip.js +113 -719
  96. package/bin/runners/runTruth.js +0 -3
  97. package/bin/runners/runValidate.js +13 -9
  98. package/bin/runners/runWatch.js +23 -14
  99. package/bin/scan.js +6 -1
  100. package/bin/vibecheck.js +204 -185
  101. package/mcp-server/deprecation-middleware.js +282 -0
  102. package/mcp-server/handlers/index.ts +15 -0
  103. package/mcp-server/handlers/tool-handler.ts +554 -0
  104. package/mcp-server/index-v1.js +698 -0
  105. package/mcp-server/index.js +210 -238
  106. package/mcp-server/lib/cache-wrapper.cjs +383 -0
  107. package/mcp-server/lib/error-envelope.js +138 -0
  108. package/mcp-server/lib/executor.ts +499 -0
  109. package/mcp-server/lib/index.ts +19 -0
  110. package/mcp-server/lib/rate-limiter.js +166 -0
  111. package/mcp-server/lib/sandbox.test.ts +519 -0
  112. package/mcp-server/lib/sandbox.ts +395 -0
  113. package/mcp-server/lib/types.ts +267 -0
  114. package/mcp-server/package.json +12 -3
  115. package/mcp-server/registry/tool-registry.js +794 -0
  116. package/mcp-server/registry/tools.json +605 -0
  117. package/mcp-server/registry.test.ts +334 -0
  118. package/mcp-server/tests/tier-gating.test.js +297 -0
  119. package/mcp-server/tier-auth.js +378 -45
  120. package/mcp-server/tools-v3.js +353 -442
  121. package/mcp-server/tsconfig.json +37 -0
  122. package/mcp-server/vibecheck-2.0-tools.js +14 -1
  123. package/package.json +1 -1
  124. package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
  125. package/bin/runners/lib/audit-logger.js +0 -532
  126. package/bin/runners/lib/authority/authorities/architecture.js +0 -364
  127. package/bin/runners/lib/authority/authorities/compliance.js +0 -341
  128. package/bin/runners/lib/authority/authorities/human.js +0 -343
  129. package/bin/runners/lib/authority/authorities/quality.js +0 -420
  130. package/bin/runners/lib/authority/authorities/security.js +0 -228
  131. package/bin/runners/lib/authority/index.js +0 -293
  132. package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
  133. package/bin/runners/lib/cli-charts.js +0 -368
  134. package/bin/runners/lib/cli-config-display.js +0 -405
  135. package/bin/runners/lib/cli-demo.js +0 -275
  136. package/bin/runners/lib/cli-errors.js +0 -438
  137. package/bin/runners/lib/cli-help-formatter.js +0 -439
  138. package/bin/runners/lib/cli-interactive-menu.js +0 -509
  139. package/bin/runners/lib/cli-prompts.js +0 -441
  140. package/bin/runners/lib/cli-scan-cards.js +0 -362
  141. package/bin/runners/lib/compliance-reporter.js +0 -710
  142. package/bin/runners/lib/conductor/index.js +0 -671
  143. package/bin/runners/lib/easy/README.md +0 -123
  144. package/bin/runners/lib/easy/index.js +0 -140
  145. package/bin/runners/lib/easy/interactive-wizard.js +0 -788
  146. package/bin/runners/lib/easy/one-click-firewall.js +0 -564
  147. package/bin/runners/lib/easy/zero-config-reality.js +0 -714
  148. package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
  149. package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
  150. package/bin/runners/lib/engines/confidence-scoring.js +0 -276
  151. package/bin/runners/lib/engines/context-detection.js +0 -264
  152. package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
  153. package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
  154. package/bin/runners/lib/engines/env-variables-engine.js +0 -458
  155. package/bin/runners/lib/engines/error-handling-engine.js +0 -437
  156. package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
  157. package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
  158. package/bin/runners/lib/engines/framework-detection.js +0 -508
  159. package/bin/runners/lib/engines/import-order-engine.js +0 -429
  160. package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
  161. package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
  162. package/bin/runners/lib/engines/orchestrator.js +0 -334
  163. package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
  164. package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
  165. package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
  166. package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
  167. package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
  168. package/bin/runners/lib/enhanced-features/index.js +0 -305
  169. package/bin/runners/lib/enhanced-output.js +0 -631
  170. package/bin/runners/lib/enterprise.js +0 -300
  171. package/bin/runners/lib/firewall/command-validator.js +0 -351
  172. package/bin/runners/lib/firewall/config.js +0 -341
  173. package/bin/runners/lib/firewall/content-validator.js +0 -519
  174. package/bin/runners/lib/firewall/index.js +0 -101
  175. package/bin/runners/lib/firewall/path-validator.js +0 -256
  176. package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
  177. package/bin/runners/lib/mcp-utils.js +0 -425
  178. package/bin/runners/lib/output/index.js +0 -1022
  179. package/bin/runners/lib/policy-engine.js +0 -652
  180. package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
  181. package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
  182. package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
  183. package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
  184. package/bin/runners/lib/polish/autofix/index.js +0 -200
  185. package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
  186. package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
  187. package/bin/runners/lib/polish/backend-checks.js +0 -148
  188. package/bin/runners/lib/polish/documentation-checks.js +0 -111
  189. package/bin/runners/lib/polish/frontend-checks.js +0 -168
  190. package/bin/runners/lib/polish/index.js +0 -71
  191. package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
  192. package/bin/runners/lib/polish/library-detection.js +0 -175
  193. package/bin/runners/lib/polish/performance-checks.js +0 -100
  194. package/bin/runners/lib/polish/security-checks.js +0 -148
  195. package/bin/runners/lib/polish/utils.js +0 -203
  196. package/bin/runners/lib/prompt-builder.js +0 -540
  197. package/bin/runners/lib/proof-certificate.js +0 -634
  198. package/bin/runners/lib/reality/accessibility-audit.js +0 -946
  199. package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
  200. package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
  201. package/bin/runners/lib/reality/performance-tracker.js +0 -1077
  202. package/bin/runners/lib/reality/scenario-generator.js +0 -1404
  203. package/bin/runners/lib/reality/visual-regression.js +0 -852
  204. package/bin/runners/lib/reality-profiler.js +0 -717
  205. package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
  206. package/bin/runners/lib/review/ai-code-review.js +0 -832
  207. package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
  208. package/bin/runners/lib/sbom-generator.js +0 -641
  209. package/bin/runners/lib/scan-output-enhanced.js +0 -512
  210. package/bin/runners/lib/security/owasp-scanner.js +0 -939
  211. package/bin/runners/lib/validators/contract-validator.js +0 -283
  212. package/bin/runners/lib/validators/dead-export-detector.js +0 -279
  213. package/bin/runners/lib/validators/dep-audit.js +0 -245
  214. package/bin/runners/lib/validators/env-validator.js +0 -319
  215. package/bin/runners/lib/validators/index.js +0 -120
  216. package/bin/runners/lib/validators/license-checker.js +0 -252
  217. package/bin/runners/lib/validators/route-validator.js +0 -290
  218. package/bin/runners/runAuthority.js +0 -528
  219. package/bin/runners/runConductor.js +0 -772
  220. package/bin/runners/runContainer.js +0 -366
  221. package/bin/runners/runEasy.js +0 -410
  222. package/bin/runners/runIaC.js +0 -372
  223. package/bin/runners/runVibe.js +0 -791
  224. 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
- };